Browse Source

Merge pull request #457 from icylogic/feature/statistics_ui

feature/statistics ui
tags/3.0
icylogic 9 years ago
parent
commit
a1ae4a6062
13 changed files with 581 additions and 441 deletions
  1. +217
    -199
      shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs
  2. +20
    -14
      shadowsocks-csharp/Controller/Service/TCPRelay.cs
  3. +23
    -10
      shadowsocks-csharp/Controller/ShadowsocksController.cs
  4. +59
    -45
      shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs
  5. +1
    -1
      shadowsocks-csharp/Data/cn.txt
  6. +5
    -0
      shadowsocks-csharp/Model/Server.cs
  7. +95
    -0
      shadowsocks-csharp/Model/StatisticsRecord.cs
  8. +8
    -46
      shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs
  9. +8
    -6
      shadowsocks-csharp/View/CalculationControl.Designer.cs
  10. +82
    -77
      shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs
  11. +48
    -32
      shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs
  12. +5
    -2
      shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx
  13. +10
    -9
      shadowsocks-csharp/shadowsocks-csharp.csproj

+ 217
- 199
shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs View File

@@ -1,159 +1,157 @@
using System;
using System.Collections.Concurrent;
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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;

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 class AvailabilityStatistics
public sealed class AvailabilityStatistics : IDisposable
{
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<Server> _servers;
private StatisticsStrategyConfiguration _config;

public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss";
private const string StatisticsFilesName = "shadowsocks.availability.json";
public static string AvailabilityStatisticsFile;

//static constructor to initialize every public static fields before refereced
static AvailabilityStatistics()
{
AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName);
}

public AvailabilityStatistics(Configuration config, StatisticsStrategyConfiguration statisticsConfig)
//arguments for ICMP tests
private int Repeat => Config.RepeatTimesNum;
public const int TimeoutMilliseconds = 500;

//records cache for current server in {_monitorInterval} minutes
private readonly ConcurrentDictionary<string, List<int>> _latencyRecords = new ConcurrentDictionary<string, List<int>>();
//speed in KiB/s
private readonly ConcurrentDictionary<string, long> _inboundCounter = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _lastInboundCounter = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, List<int>> _inboundSpeedRecords = new ConcurrentDictionary<string, List<int>>();
private readonly ConcurrentDictionary<string, long> _outboundCounter = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _lastOutboundCounter = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, List<int>> _outboundSpeedRecords = new ConcurrentDictionary<string, List<int>>();

//tasks
private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1);
private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2);
private Timer _recorder; //analyze and save cached records to RawStatistics and filter records
private TimeSpan RecordingInterval => TimeSpan.FromMinutes(Config.DataCollectionMinutes);
private Timer _speedMonior;
private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1);
//private Timer _writer; //write RawStatistics to file
//private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1);

private ShadowsocksController _controller;
private StatisticsStrategyConfiguration Config => _controller.StatisticsConfiguration;

// Static Singleton Initialization
public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics();
public Statistics RawStatistics { get; private set; }
public Statistics FilteredStatistics { get; private set; }

private AvailabilityStatistics()
{
UpdateConfiguration(config, statisticsConfig);
RawStatistics = new Statistics();
}

public bool Set(StatisticsStrategyConfiguration config)
internal void UpdateConfiguration(ShadowsocksController controller)
{
_config = config;
_controller = controller;
Reset();
try
{
if (config.StatisticsEnabled)
if (Config.StatisticsEnabled)
{
if (_timer?.Change(DelayBeforeStart, Interval) == null)
{
_state = new State();
_timer = new Timer(Run, _state, DelayBeforeStart, Interval);
}
StartTimerWithoutState(ref _recorder, Run, RecordingInterval);
LoadRawStatistics();
StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval);
}
else
{
_timer?.Dispose();
_recorder?.Dispose();
_speedMonior?.Dispose();
}
return true;
}
catch (Exception e)
{
Logging.LogUsefulException(e);
return false;
}
}

//hardcode
//TODO: backup reliable isp&geolocation provider or a local database is required
public static async Task<DataList> GetGeolocationAndIsp()
private void StartTimerWithoutState(ref Timer timer, TimerCallback callback, TimeSpan interval)
{
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
if (timer?.Change(_delayBeforeStart, interval) == null)
{
new DataUnit(State.Geolocation, State.Unknown),
new DataUnit(State.ISP, State.Unknown)
};
timer = new Timer(callback, null, _delayBeforeStart, interval);
}
}

private static async Task<DataList> GetInfoFromAPI(string API)
private void UpdateSpeed(object _)
{
string jsonString;
try
{
jsonString = await new HttpClient().GetStringAsync(API);
}
catch (HttpRequestException e)
foreach (var kv in _lastInboundCounter)
{
Logging.LogUsefulException(e);
return null;
}
JObject obj;
try
{
obj = JObject.Parse(jsonString);
}
catch (JsonReaderException)
{
return null;
var id = kv.Key;

var lastInbound = kv.Value;
var inbound = _inboundCounter[id];
var bytes = inbound - lastInbound;
_lastInboundCounter[id] = inbound;
var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds);
_inboundSpeedRecords.GetOrAdd(id, new List<int> {inboundSpeed}).Add(inboundSpeed);

var lastOutbound = _lastOutboundCounter[id];
var outbound = _outboundCounter[id];
bytes = outbound - lastOutbound;
_lastOutboundCounter[id] = outbound;
var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds);
_outboundSpeedRecords.GetOrAdd(id, new List<int> {outboundSpeed}).Add(outboundSpeed);

Logging.Debug(
$"{id}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords[id].Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords[id].Max()} KiB/s");
}
string country = (string)obj["country"];
string city = (string)obj["city"];
string isp = (string)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<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, Timeout);
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>("data", reply.Buffer.ToString()); // The data of reply
});
Thread.Sleep(Timeout + new Random().Next() % Timeout);
var reply = await ping.SendTaskAsync(IP, 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)
{
@@ -161,52 +159,82 @@ 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 void Run(object obj)
private void Reset()
{
LoadRawStatistics();
_inboundSpeedRecords.Clear();
_outboundSpeedRecords.Clear();
_latencyRecords.Clear();
}

private void Run(object _)
{
UpdateRecords();
Save();
Reset();
FilterRawStatistics();
evaluate();
}

private async void evaluate()
private async void UpdateRecords()
{
var geolocationAndIsp = GetGeolocationAndIsp();
foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest)))
var records = new Dictionary<string, StatisticsRecord>();

foreach (var server in _controller.GetCurrentConfiguration().configs)
{
if (dataLists == null) continue;
foreach (var dataList in dataLists.Where(dataList => dataList != null))
var id = server.Identifier();
List<int> inboundSpeedRecords = null;
List<int> outboundSpeedRecords = null;
List<int> latencyRecords = null;
_inboundSpeedRecords.TryGetValue(id, out inboundSpeedRecords);
_outboundSpeedRecords.TryGetValue(id, out outboundSpeedRecords);
_latencyRecords.TryGetValue(id, out latencyRecords);
records.Add(id, new StatisticsRecord(id, inboundSpeedRecords, outboundSpeedRecords, latencyRecords));
}

if (Config.Ping)
{
var icmpResults = await TaskEx.WhenAll(_controller.GetCurrentConfiguration().configs.Select(ICMPTest));
foreach (var result in icmpResults.Where(result => result != null))
{
await geolocationAndIsp;
Append(dataList, geolocationAndIsp.Result);
records[result.Server.Identifier()].SetResponse(result.RoundtripTime);
}
}

foreach (var kv in records.Where(kv => !kv.Value.IsEmptyData()))
{
AppendRecord(kv.Key, kv.Value);
}
}

private static void Append(DataList dataList, IEnumerable<DataUnit> extra)
private void AppendRecord(string serverIdentifier, StatisticsRecord record)
{
var data = dataList.Concat(extra);
var dataLine = string.Join(Delimiter, data.Select(kv => kv.Value).ToArray());
string[] lines;
if (!File.Exists(AvailabilityStatisticsFile))
List<StatisticsRecord> records;
if (!RawStatistics.TryGetValue(serverIdentifier, out records))
{
var headerLine = string.Join(Delimiter, data.Select(kv => kv.Key).ToArray());
lines = new[] { headerLine, dataLine };
records = new List<StatisticsRecord>();
}
else
records.Add(record);
RawStatistics[serverIdentifier] = records;
}

private void Save()
{
if (RawStatistics.Count == 0)
{
lines = new[] { dataLine };
return;
}
try
{
File.AppendAllLines(AvailabilityStatisticsFile, lines);
var content = JsonConvert.SerializeObject(RawStatistics, Formatting.None);
File.WriteAllText(AvailabilityStatisticsFile, content);
}
catch (IOException e)
{
@@ -214,46 +242,28 @@ namespace Shadowsocks.Controller
}
}

internal void UpdateConfiguration(Configuration config, StatisticsStrategyConfiguration statisticsConfig)
private bool IsValidRecord(StatisticsRecord record)
{
Set(statisticsConfig);
_servers = config.configs;
if (Config.ByHourOfDay)
{
if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false;
}
return true;
}

private async void FilterRawStatistics()
private void FilterRawStatistics()
{
if (RawStatistics == null) return;
if (FilteredStatistics == null)
{
FilteredStatistics = new Statistics();
}
foreach (IEnumerable<RawStatisticsData> rawData in RawStatistics.Values)

foreach (var serverAndRecords in RawStatistics)
{
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 == 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;
}
}

@@ -265,77 +275,85 @@ namespace Shadowsocks.Controller
Logging.Debug($"loading statistics from {path}");
if (!File.Exists(path))
{
try {
using (FileStream fs = File.Create(path))
{
//do nothing
}
}catch(Exception e)
using (File.Create(path))
{
Logging.LogUsefulException(e);
}
if (!File.Exists(path)) {
Console.WriteLine($"statistics file does not exist, try to reload {RetryInterval / 60 / 1000} minutes later");
_timer.Change(RetryInterval, Interval);
return;
//do nothing
}
}
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);
var content = File.ReadAllText(path);
RawStatistics = JsonConvert.DeserializeObject<Statistics>(content) ?? RawStatistics;
}
catch (Exception e)
{
Logging.LogUsefulException(e);
Console.WriteLine($"failed to load statistics; try to reload {_retryInterval.TotalMinutes} minutes later");
_recorder.Change(_retryInterval, RecordingInterval);
}
}

private DateTime ParseExactOrUnknown(string str)
private static int GetSpeedInKiBPerSecond(long bytes, double seconds)
{
DateTime dateTime;
return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) ? UnknownDateTime : dateTime;
var result = (int) (bytes/seconds)/1024;
return result;
}

public class State
private class ICMPResult
{
public DataList dataList = new DataList();
public const string Geolocation = "Geolocation";
public const string ISP = "ISP";
public const string Unknown = "Unknown";
internal readonly List<int?> RoundtripTime = new List<int?>();
internal readonly Server Server;

internal ICMPResult(Server server)
{
Server = server;
}
}

public class RawStatisticsData
public void Dispose()
{
public DateTime Timestamp;
public string ServerName;
public string ICMPStatus;
public int RoundtripTime;
public string Geolocation;
public string ISP;
_recorder.Dispose();
_speedMonior.Dispose();
}

public class StatisticsData
public void UpdateLatency(Server server, int latency)
{
public float PackageLoss;
public int AverageResponse;
public int MinResponse;
public int MaxResponse;
List<int> records;
_latencyRecords.TryGetValue(server.Identifier(), out records);
if (records == null)
{
records = new List<int>();
}
records.Add(latency);
_latencyRecords[server.Identifier()] = records;
}

public void UpdateInboundCounter(Server server, long n)
{
long count;
if (_inboundCounter.TryGetValue(server.Identifier(), out count))
{
count += n;
}
else
{
count = n;
_lastInboundCounter[server.Identifier()] = 0;
}
_inboundCounter[server.Identifier()] = count;
}

public void UpdateOutboundCounter(Server server, long n)
{
long count;
if (_outboundCounter.TryGetValue(server.Identifier(), out count))
{
count += n;
}
else
{
count = n;
_lastOutboundCounter[server.Identifier()] = 0;
}
_outboundCounter[server.Identifier()] = count;
}
}
}

+ 20
- 14
shadowsocks-csharp/Controller/Service/TCPRelay.cs View File

@@ -69,14 +69,19 @@ namespace Shadowsocks.Controller
return true;
}
public void UpdateInboundCounter(long n)
public void UpdateInboundCounter(Server server, long n)
{
_controller.UpdateInboundCounter(n);
_controller.UpdateInboundCounter(server, n);
}
public void UpdateOutboundCounter(long n)
public void UpdateOutboundCounter(Server server, long n)
{
_controller.UpdateOutboundCounter(n);
_controller.UpdateOutboundCounter(server, n);
}
public void UpdateLatency(Server server, TimeSpan latency)
{
_controller.UpdateLatency(server, latency);
}
}
@@ -126,6 +131,9 @@ namespace Shadowsocks.Controller
private object decryptionLock = new object();
private DateTime _startConnectTime;
private DateTime _startReceivingTime;
private DateTime _startSendingTime;
private int _bytesToSend;
private TCPRelay tcprelay; // TODO: tcprelay ?= relay
public TCPHandler(TCPRelay tcprelay)
@@ -480,10 +488,8 @@ namespace Shadowsocks.Controller
var latency = DateTime.Now - _startConnectTime;
IStrategy strategy = controller.GetCurrentStrategy();
if (strategy != null)
{
strategy.UpdateLatency(server, latency);
}
strategy?.UpdateLatency(server, latency);
tcprelay.UpdateLatency(server, latency);
StartPipe();
}
@@ -513,6 +519,7 @@ namespace Shadowsocks.Controller
}
try
{
_startReceivingTime = DateTime.Now;
remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, new AsyncCallback(PipeRemoteReceiveCallback), null);
connection.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, new AsyncCallback(PipeConnectionReceiveCallback), null);
}
@@ -533,7 +540,7 @@ namespace Shadowsocks.Controller
{
int bytesRead = remote.EndReceive(ar);
totalRead += bytesRead;
tcprelay.UpdateInboundCounter(bytesRead);
tcprelay.UpdateInboundCounter(server, bytesRead);
if (bytesRead > 0)
{
@@ -600,14 +607,13 @@ namespace Shadowsocks.Controller
encryptor.Encrypt(connetionRecvBuffer, bytesRead, connetionSendBuffer, out bytesToSend);
}
Logging.Debug(remote, bytesToSend, "TCP Relay", "@PipeConnectionReceiveCallback() (upload)");
tcprelay.UpdateOutboundCounter(bytesToSend);
tcprelay.UpdateOutboundCounter(server, bytesToSend);
_startSendingTime = DateTime.Now;
_bytesToSend = bytesToSend;
remote.BeginSend(connetionSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeRemoteSendCallback), null);
IStrategy strategy = controller.GetCurrentStrategy();
if (strategy != null)
{
strategy.UpdateLastWrite(server);
}
strategy?.UpdateLastWrite(server);
}
else
{


+ 23
- 10
shadowsocks-csharp/Controller/ShadowsocksController.cs View File

@@ -5,7 +5,7 @@ using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Shadowsocks.Controller.Strategy;
@@ -30,7 +30,7 @@ namespace Shadowsocks.Controller
private StrategyManager _strategyManager;
private PolipoRunner polipoRunner;
private GFWListUpdater gfwListUpdater;
public AvailabilityStatistics availabilityStatistics { get; private set; }
public AvailabilityStatistics availabilityStatistics = AvailabilityStatistics.Instance;
public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; }
public long inboundCounter = 0;
@@ -268,7 +268,7 @@ namespace Shadowsocks.Controller
public void UpdateStatisticsConfiguration(bool enabled)
{
if (availabilityStatistics == null) return;
availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration);
availabilityStatistics.UpdateConfiguration(this);
_config.availabilityStatistics = enabled;
SaveConfig(_config);
}
@@ -307,14 +307,30 @@ namespace Shadowsocks.Controller
Configuration.Save(_config);
}
public void UpdateInboundCounter(long n)
public void UpdateLatency(Server server, TimeSpan latency)
{
if (_config.availabilityStatistics)
{
new Task(() => availabilityStatistics.UpdateLatency(server, (int) latency.TotalMilliseconds)).Start();
}
}
public void UpdateInboundCounter(Server server, long n)
{
Interlocked.Add(ref inboundCounter, n);
if (_config.availabilityStatistics)
{
new Task(() => availabilityStatistics.UpdateInboundCounter(server, n)).Start();
}
}
public void UpdateOutboundCounter(long n)
public void UpdateOutboundCounter(Server server, long n)
{
Interlocked.Add(ref outboundCounter, n);
if (_config.availabilityStatistics)
{
new Task(() => availabilityStatistics.UpdateOutboundCounter(server, n)).Start();
}
}
protected void Reload()
@@ -341,11 +357,7 @@ namespace Shadowsocks.Controller
gfwListUpdater.Error += pacServer_PACUpdateError;
}
if (availabilityStatistics == null)
{
availabilityStatistics = new AvailabilityStatistics(_config, StatisticsConfiguration);
}
availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration);
availabilityStatistics.UpdateConfiguration(this);
if (_listener != null)
{
@@ -501,5 +513,6 @@ namespace Shadowsocks.Controller
Thread.Sleep(30 * 1000);
}
}
}
}

+ 59
- 45
shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;

using Newtonsoft.Json;
@@ -11,12 +10,15 @@ using Shadowsocks.Model;

namespace Shadowsocks.Controller.Strategy
{
class StatisticsStrategy : IStrategy
using Statistics = Dictionary<string, List<StatisticsRecord>>;

internal class StatisticsStrategy : IStrategy, IDisposable
{
private readonly ShadowsocksController _controller;
private Server _currentServer;
private readonly Timer _timer;
private Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>> _filteredStatistics;
private Statistics _filteredStatistics;
private AvailabilityStatistics Service => _controller.availabilityStatistics;
private int ChoiceKeptMilliseconds
=> (int)TimeSpan.FromMinutes(_controller.StatisticsConfiguration.ChoiceKeptMinutes).TotalMilliseconds;

@@ -24,7 +26,7 @@ namespace Shadowsocks.Controller.Strategy
{
_controller = controller;
var servers = controller.GetCurrentConfiguration().configs;
var randomIndex = new Random().Next() % servers.Count();
var randomIndex = new Random().Next() % servers.Count;
_currentServer = servers[randomIndex]; //choose a server randomly at first
_timer = new Timer(ReloadStatisticsAndChooseAServer);
}
@@ -39,36 +41,40 @@ namespace Shadowsocks.Controller.Strategy

private void LoadStatistics()
{
_filteredStatistics = _controller.availabilityStatistics.RawStatistics ?? _filteredStatistics ?? new Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>();
_filteredStatistics =
Service.FilteredStatistics ??
Service.RawStatistics ??
_filteredStatistics;
}

//return the score by data
//server with highest score will be choosen
private float GetScore(string serverName)
private float? GetScore(string identifier, List<StatisticsRecord> records)
{
var config = _controller.StatisticsConfiguration;
List<AvailabilityStatistics.RawStatisticsData> dataList;
if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out dataList)) return 0;
var successTimes = (float)dataList.Count(data => data.ICMPStatus == IPStatus.Success.ToString());
var timedOutTimes = (float)dataList.Count(data => data.ICMPStatus == IPStatus.TimedOut.ToString());
var statisticsData = new AvailabilityStatistics.StatisticsData
float? score = null;

var averageRecord = new StatisticsRecord(identifier,
records.Where(record => record.MaxInboundSpeed != null).Select(record => record.MaxInboundSpeed.Value).ToList(),
records.Where(record => record.MaxOutboundSpeed != null).Select(record => record.MaxOutboundSpeed.Value).ToList(),
records.Where(record => record.AverageLatency != null).Select(record => record.AverageLatency.Value).ToList());
averageRecord.SetResponse(records.Select(record => record.AverageResponse).ToList());

foreach (var calculation in config.Calculations)
{
var name = calculation.Key;
var field = typeof (StatisticsRecord).GetField(name);
dynamic value = field?.GetValue(averageRecord);
var factor = calculation.Value;
if (value == null || factor.Equals(0)) continue;
score = score ?? 0;
score += value * factor;
}

if (score != null)
{
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)}");
Logging.Debug($"Highest score: {score} {JsonConvert.SerializeObject(averageRecord, Formatting.Indented)}");
}
return score;
}

@@ -80,15 +86,25 @@ namespace Shadowsocks.Controller.Strategy
}
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);
var serversWithStatistics = (from server in servers
let id = server.Identifier()
where _filteredStatistics.ContainsKey(id)
let score = GetScore(server.Identifier(), _filteredStatistics[server.Identifier()])
where score != null
select new
{
server,
score
}).ToArray();

if (serversWithStatistics.Length < 2)
{
LogWhenEnabled("no enough statistics data or all factors in calculations are 0");
return;
}

var bestResult = serversWithStatistics
.Aggregate((server1, server2) => server1.score > server2.score ? server1 : server2);

LogWhenEnabled($"Switch to server: {bestResult.server.FriendlyName()} by statistics: score {bestResult.score}");
_currentServer = bestResult.server;
@@ -109,18 +125,14 @@ namespace Shadowsocks.Controller.Strategy

public string ID => "com.shadowsocks.strategy.scbs";

public string Name => I18N.GetString("Choose By Total Package Loss");
public string Name => I18N.GetString("Choose by statistics");

public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint)
{
var oldServer = _currentServer;
if (oldServer == null)
if (_currentServer == null)
{
ChooseNewServer(_controller.GetCurrentConfiguration().configs);
}
if (oldServer != _currentServer)
{
}
return _currentServer; //current server cached for CachedInterval
}

@@ -137,17 +149,19 @@ namespace Shadowsocks.Controller.Strategy

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
}

public void Dispose()
{
_timer.Dispose();
}
}
}

+ 1
- 1
shadowsocks-csharp/Data/cn.txt View File

@@ -30,7 +30,7 @@ Quit=退出
Edit Servers=编辑服务器
Load Balance=负载均衡
High Availability=高可用
Choose By Total Package Loss=累计丢包率
Choose by statistics=根据统计

# Config Form



+ 5
- 0
shadowsocks-csharp/Model/Server.cs View File

@@ -95,5 +95,10 @@ namespace Shadowsocks.Model
throw new FormatException();
}
}
public string Identifier()
{
return server + ':' + server_port;
}
}
}

+ 95
- 0
shadowsocks-csharp/Model/StatisticsRecord.cs View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Shadowsocks.Model
{
// Simple processed records for a short period of time
public class StatisticsRecord
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public string ServerIdentifier { get; set; }

// in ping-only records, these fields would be null
public int? AverageLatency;
public int? MinLatency;
public int? MaxLatency;

private bool EmptyLatencyData => (AverageLatency == null) && (MinLatency == null) && (MaxLatency == null);

public int? AverageInboundSpeed;
public int? MinInboundSpeed;
public int? MaxInboundSpeed;

private bool EmptyInboundSpeedData
=> (AverageInboundSpeed == null) && (MinInboundSpeed == null) && (MaxInboundSpeed == null);

public int? AverageOutboundSpeed;
public int? MinOutboundSpeed;
public int? MaxOutboundSpeed;

private bool EmptyOutboundSpeedData
=> (AverageOutboundSpeed == null) && (MinOutboundSpeed == null) && (MaxOutboundSpeed == null);

// if user disabled ping test, response would be null
public int? AverageResponse;
public int? MinResponse;
public int? MaxResponse;
public float? PackageLoss;

private bool EmptyResponseData
=> (AverageResponse == null) && (MinResponse == null) && (MaxResponse == null) && (PackageLoss == null);

public bool IsEmptyData() {
return EmptyInboundSpeedData && EmptyOutboundSpeedData && EmptyResponseData && EmptyLatencyData;
}

public StatisticsRecord()
{
}

public StatisticsRecord(string identifier, ICollection<int> inboundSpeedRecords, ICollection<int> outboundSpeedRecords, ICollection<int> latencyRecords)
{
ServerIdentifier = identifier;
var inbound = inboundSpeedRecords?.Where(s => s > 0).ToList();
if (inbound != null && inbound.Any())
{
AverageInboundSpeed = (int) inbound.Average();
MinInboundSpeed = inbound.Min();
MaxInboundSpeed = inbound.Max();
}
var outbound = outboundSpeedRecords?.Where(s => s > 0).ToList();
if (outbound!= null && outbound.Any())
{
AverageOutboundSpeed = (int) outbound.Average();
MinOutboundSpeed = outbound.Min();
MaxOutboundSpeed = outbound.Max();
}
var latency = latencyRecords?.Where(s => s > 0).ToList();
if (latency!= null && latency.Any())
{
AverageLatency = (int) latency.Average();
MinLatency = latency.Min();
MaxLatency = latency.Max();
}
}

public StatisticsRecord(string identifier, ICollection<int?> responseRecords)
{
ServerIdentifier = identifier;
SetResponse(responseRecords);
}

public void SetResponse(ICollection<int?> responseRecords)
{
if (responseRecords == null) return;
var records = responseRecords.Where(response => response != null).Select(response => response.Value).ToList();
if (!records.Any()) return;
AverageResponse = (int?) records.Average();
MinResponse = records.Min();
MaxResponse = records.Max();
PackageLoss = responseRecords.Count(response => response != null)/(float) responseRecords.Count;
}
}
}

+ 8
- 46
shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs View File

@@ -13,13 +13,13 @@ 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;
public static readonly string ID = "com.shadowsocks.strategy.statistics";
public bool StatisticsEnabled { get; set; } = true;
public bool ByHourOfDay { get; set; } = true;
public bool Ping { get; set; }
public int ChoiceKeptMinutes { get; set; } = 10;
public int DataCollectionMinutes { get; set; } = 10;
public int RepeatTimesNum { get; set; } = 4;

private const string ConfigFile = "statistics-config.json";

@@ -61,46 +61,8 @@ namespace Shadowsocks.Model

public StatisticsStrategyConfiguration()
{
var availabilityStatisticsType = typeof(AvailabilityStatistics);
var statisticsData = availabilityStatisticsType.GetNestedType("StatisticsData");
var properties = statisticsData.GetFields(BindingFlags.Instance | BindingFlags.Public);
var properties = typeof(StatisticsRecord).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; }
}
}
}

+ 8
- 6
shadowsocks-csharp/View/CalculationControl.Designer.cs View File

@@ -38,13 +38,14 @@
// factorNum
//
this.factorNum.DecimalPlaces = 2;
this.factorNum.Dock = System.Windows.Forms.DockStyle.Right;
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.Location = new System.Drawing.Point(236, 0);
this.factorNum.Minimum = new decimal(new int[] {
1000,
0,
@@ -58,7 +59,7 @@
//
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.Location = new System.Drawing.Point(202, 2);
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);
@@ -69,7 +70,7 @@
//
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.Location = new System.Drawing.Point(5, 2);
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);
@@ -80,7 +81,7 @@
//
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.Location = new System.Drawing.Point(39, 6);
this.valueLabel.Name = "valueLabel";
this.valueLabel.Size = new System.Drawing.Size(118, 24);
this.valueLabel.TabIndex = 7;
@@ -88,14 +89,15 @@
//
// CalculationControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
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.Margin = new System.Windows.Forms.Padding(0);
this.Name = "CalculationControl";
this.Size = new System.Drawing.Size(380, 46);
this.Size = new System.Drawing.Size(322, 34);
((System.ComponentModel.ISupportInitialize)(this.factorNum)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();


+ 82
- 77
shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs View File

@@ -33,11 +33,12 @@
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();
System.Windows.Forms.DataVisualization.Charting.Series series3 = new System.Windows.Forms.DataVisualization.Charting.Series();
this.StatisticsChart = new System.Windows.Forms.DataVisualization.Charting.Chart();
this.byISPCheckBox = new System.Windows.Forms.CheckBox();
this.PingCheckBox = new System.Windows.Forms.CheckBox();
this.bindingConfiguration = new System.Windows.Forms.BindingSource(this.components);
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();
@@ -57,8 +58,9 @@
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);
this.CalculatinTip = new System.Windows.Forms.ToolTip(this.components);
((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit();
this.chartModeSelector.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
@@ -75,7 +77,6 @@
this.splitContainer3.Panel1.SuspendLayout();
this.splitContainer3.Panel2.SuspendLayout();
this.splitContainer3.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit();
this.SuspendLayout();
//
// StatisticsChart
@@ -83,6 +84,7 @@
this.StatisticsChart.BackColor = System.Drawing.Color.Transparent;
chartArea1.AxisX.MajorGrid.Enabled = false;
chartArea1.AxisY.MajorGrid.Enabled = false;
chartArea1.AxisY2.Enabled = System.Windows.Forms.DataVisualization.Charting.AxisEnabled.False;
chartArea1.AxisY2.MajorGrid.Enabled = false;
chartArea1.BackColor = System.Drawing.Color.Transparent;
chartArea1.Name = "DataArea";
@@ -96,40 +98,58 @@
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.Color = System.Drawing.Color.DarkGray;
series1.Legend = "ChartLegend";
series1.Name = "Package Loss";
series1.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime;
series1.YValuesPerPoint = 2;
series2.BorderWidth = 4;
series1.Name = "Speed";
series1.ToolTip = "#VALX\\nMax inbound speed\\n#VAL KiB/s";
series1.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time;
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.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Bubble;
series2.Color = System.Drawing.Color.Crimson;
series2.CustomProperties = "EmptyPointValue=Zero";
series2.Legend = "ChartLegend";
series2.Name = "Ping";
series2.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime;
series2.Name = "Package Loss";
series2.ToolTip = "#VALX\\n#VAL%";
series2.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time;
series2.YAxisType = System.Windows.Forms.DataVisualization.Charting.AxisType.Secondary;
series2.YValuesPerPoint = 2;
series3.BorderWidth = 5;
series3.ChartArea = "DataArea";
series3.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
series3.Color = System.Drawing.Color.DodgerBlue;
series3.Legend = "ChartLegend";
series3.MarkerSize = 10;
series3.MarkerStyle = System.Windows.Forms.DataVisualization.Charting.MarkerStyle.Circle;
series3.Name = "Ping";
series3.ToolTip = "#VALX\\n#VAL ms";
series3.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time;
this.StatisticsChart.Series.Add(series1);
this.StatisticsChart.Series.Add(series2);
this.StatisticsChart.Size = new System.Drawing.Size(1077, 303);
this.StatisticsChart.Series.Add(series3);
this.StatisticsChart.Size = new System.Drawing.Size(978, 429);
this.StatisticsChart.TabIndex = 2;
//
// byISPCheckBox
// PingCheckBox
//
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;
this.PingCheckBox.AutoSize = true;
this.PingCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "Ping", true));
this.PingCheckBox.Location = new System.Drawing.Point(13, 54);
this.PingCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10);
this.PingCheckBox.Name = "PingCheckBox";
this.PingCheckBox.Size = new System.Drawing.Size(124, 31);
this.PingCheckBox.TabIndex = 5;
this.PingCheckBox.Text = "Ping Test";
this.PingCheckBox.UseVisualStyleBackColor = true;
this.PingCheckBox.CheckedChanged += new System.EventHandler(this.PingCheckBox_CheckedChanged);
//
// bindingConfiguration
//
this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration);
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(8, 136);
this.label2.Location = new System.Drawing.Point(9, 206);
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);
@@ -139,48 +159,35 @@
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(285, 136);
this.label3.Location = new System.Drawing.Point(286, 206);
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.Location = new System.Drawing.Point(729, 188);
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.Size = new System.Drawing.Size(234, 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);
@@ -188,11 +195,13 @@
// dayMode
//
this.dayMode.AutoSize = true;
this.dayMode.Checked = 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.TabStop = true;
this.dayMode.Text = "24h";
this.dayMode.UseVisualStyleBackColor = true;
this.dayMode.CheckedChanged += new System.EventHandler(this.dayMode_CheckedChanged);
@@ -217,8 +226,8 @@
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.Size = new System.Drawing.Size(978, 744);
this.splitContainer1.SplitterDistance = 305;
this.splitContainer1.SplitterWidth = 10;
this.splitContainer1.TabIndex = 12;
//
@@ -242,14 +251,13 @@
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.PingCheckBox);
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.Size = new System.Drawing.Size(978, 305);
this.splitContainer2.SplitterDistance = 384;
this.splitContainer2.SplitterWidth = 5;
this.splitContainer2.TabIndex = 7;
@@ -257,7 +265,7 @@
// label9
//
this.label9.AutoSize = true;
this.label9.Location = new System.Drawing.Point(8, 175);
this.label9.Location = new System.Drawing.Point(9, 164);
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);
@@ -268,7 +276,7 @@
//
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.Location = new System.Drawing.Point(286, 165);
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);
@@ -283,7 +291,7 @@
0,
0,
0});
this.dataCollectionMinutesNum.Location = new System.Drawing.Point(176, 173);
this.dataCollectionMinutesNum.Location = new System.Drawing.Point(177, 162);
this.dataCollectionMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.dataCollectionMinutesNum.Maximum = new decimal(new int[] {
120,
@@ -291,7 +299,7 @@
0,
0});
this.dataCollectionMinutesNum.Minimum = new decimal(new int[] {
5,
1,
0,
0,
0});
@@ -324,7 +332,7 @@
0,
0,
0});
this.choiceKeptMinutesNum.Location = new System.Drawing.Point(176, 134);
this.choiceKeptMinutesNum.Location = new System.Drawing.Point(177, 204);
this.choiceKeptMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.choiceKeptMinutesNum.Maximum = new decimal(new int[] {
120,
@@ -332,7 +340,7 @@
0,
0});
this.choiceKeptMinutesNum.Minimum = new decimal(new int[] {
5,
1,
0,
0,
0});
@@ -349,7 +357,7 @@
//
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.Location = new System.Drawing.Point(13, 127);
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);
@@ -360,7 +368,7 @@
// 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.Location = new System.Drawing.Point(34, 84);
this.repeatTimesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.repeatTimesNum.Maximum = new decimal(new int[] {
10,
@@ -380,7 +388,7 @@
//
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.Location = new System.Drawing.Point(139, 86);
this.label6.Name = "label6";
this.label6.Size = new System.Drawing.Size(201, 27);
this.label6.TabIndex = 13;
@@ -403,20 +411,21 @@
// 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.Size = new System.Drawing.Size(589, 305);
this.splitContainer3.SplitterDistance = 42;
this.splitContainer3.SplitterWidth = 1;
this.splitContainer3.TabIndex = 6;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(5, 12);
this.label1.Location = new System.Drawing.Point(5, 9);
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.Size = new System.Drawing.Size(137, 27);
this.label1.TabIndex = 0;
this.label1.Text = "Design evaluation method";
this.label1.Text = "Final Score =";
this.CalculatinTip.SetToolTip(this.label1, "(The server with the highest score would be choosen)");
//
// calculationContainer
//
@@ -425,23 +434,23 @@
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.Size = new System.Drawing.Size(589, 262);
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.Location = new System.Drawing.Point(729, 151);
this.serverSelector.Name = "serverSelector";
this.serverSelector.Size = new System.Drawing.Size(260, 35);
this.serverSelector.Size = new System.Drawing.Size(233, 35);
this.serverSelector.TabIndex = 6;
this.serverSelector.SelectedIndexChanged += new System.EventHandler(this.serverSelector_SelectedIndexChanged);
this.serverSelector.SelectionChangeCommitted += new System.EventHandler(this.serverSelector_SelectionChangeCommitted);
//
// 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.Location = new System.Drawing.Point(861, 370);
this.CancelButton.Name = "CancelButton";
this.CancelButton.Size = new System.Drawing.Size(101, 41);
this.CancelButton.TabIndex = 5;
@@ -452,7 +461,7 @@
// 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.Location = new System.Drawing.Point(754, 370);
this.OKButton.Name = "OKButton";
this.OKButton.Size = new System.Drawing.Size(101, 41);
this.OKButton.TabIndex = 4;
@@ -460,23 +469,20 @@
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.ClientSize = new System.Drawing.Size(978, 744);
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.MinimumSize = new System.Drawing.Size(1000, 800);
this.Name = "StatisticsStrategyConfigurationForm";
this.Text = "StatisticsStrategyConfigurationForm";
((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit();
this.chartModeSelector.ResumeLayout(false);
this.chartModeSelector.PerformLayout();
this.splitContainer1.Panel1.ResumeLayout(false);
@@ -496,17 +502,15 @@
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.CheckBox PingCheckBox;
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;
@@ -527,5 +531,6 @@
private new System.Windows.Forms.Button CancelButton;
private System.Windows.Forms.Button OKButton;
private System.Windows.Forms.ComboBox serverSelector;
private System.Windows.Forms.ToolTip CalculatinTip;
}
}

+ 48
- 32
shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs View File

@@ -2,9 +2,8 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net.NetworkInformation;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using Shadowsocks.Controller;
using Shadowsocks.Model;

@@ -14,23 +13,30 @@ namespace Shadowsocks.View
{
private readonly ShadowsocksController _controller;
private StatisticsStrategyConfiguration _configuration;
private DataTable _dataTable = new DataTable();
private readonly DataTable _dataTable = new DataTable();
private List<string> _servers;
private readonly Series _speedSeries;
private readonly Series _packageLossSeries;
private readonly Series _pingSeries;

public StatisticsStrategyConfigurationForm(ShadowsocksController controller)
{
if (controller == null) return;
InitializeComponent();
_speedSeries = StatisticsChart.Series["Speed"];
_packageLossSeries = StatisticsChart.Series["Package Loss"];
_pingSeries = StatisticsChart.Series["Ping"];
_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();
_servers = configs.Select(server => server.Identifier()).ToList();
_configuration = _controller.StatisticsConfiguration
?? new StatisticsStrategyConfiguration();
if (_configuration.Calculations == null)
@@ -51,15 +57,20 @@ namespace Shadowsocks.View
serverSelector.DataSource = _servers;

_dataTable.Columns.Add("Timestamp", typeof(DateTime));
_dataTable.Columns.Add("Package Loss", typeof(int));
_dataTable.Columns.Add("Ping", typeof(int));
_dataTable.Columns.Add("Speed", typeof (int));
_speedSeries.XValueMember = "Timestamp";
_speedSeries.YValueMembers = "Speed";

// might be empty
_dataTable.Columns.Add("Package Loss", typeof (int));
_dataTable.Columns.Add("Ping", typeof (int));
_packageLossSeries.XValueMember = "Timestamp";
_packageLossSeries.YValueMembers = "Package Loss";
_pingSeries.XValueMember = "Timestamp";
_pingSeries.YValueMembers = "Ping";

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();
LoadChartData();
StatisticsChart.DataBind();
}

@@ -79,24 +90,30 @@ namespace Shadowsocks.View
Close();
}

private void loadChartData()
private void LoadChartData()
{
string serverName = _servers[serverSelector.SelectedIndex];
var serverName = _servers[serverSelector.SelectedIndex];
_dataTable.Rows.Clear();

//return directly when no data is usable
if (_controller.availabilityStatistics?.FilteredStatistics == null) return;
List<AvailabilityStatistics.RawStatisticsData> statistics;
List<StatisticsRecord> statistics;
if (!_controller.availabilityStatistics.FilteredStatistics.TryGetValue(serverName, out statistics)) return;
IEnumerable<IGrouping<int, AvailabilityStatistics.RawStatisticsData>> dataGroups;
IEnumerable<IGrouping<int, StatisticsRecord>> dataGroups;
if (allMode.Checked)
{
_pingSeries.XValueType = ChartValueType.DateTime;
_packageLossSeries.XValueType = ChartValueType.DateTime;
_speedSeries.XValueType = ChartValueType.DateTime;
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";
StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "g";
StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "g";
}
else
{
_pingSeries.XValueType = ChartValueType.Time;
_packageLossSeries.XValueType = ChartValueType.Time;
_speedSeries.XValueType = ChartValueType.Time;
dataGroups = statistics.GroupBy(data => data.Timestamp.Hour);
StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "HH:00";
StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "HH:00";
@@ -105,37 +122,36 @@ namespace Shadowsocks.View
orderby dataGroup.Key
select new
{
Timestamp = dataGroup.First().Timestamp,
Ping = (int)dataGroup.Average(data => data.RoundtripTime),
PackageLoss = (int)
(dataGroup.Count(data => data.ICMPStatus == IPStatus.TimedOut.ToString())
/ (float)dataGroup.Count() * 100)
dataGroup.First().Timestamp,
Speed = dataGroup.Max(data => data.MaxInboundSpeed) ?? 0,
Ping = (int) (dataGroup.Average(data => data.AverageResponse) ?? 0),
PackageLossPercentage = (int) (dataGroup.Average(data => data.PackageLoss) ?? 0) * 100
};
foreach (var data in finalData)
foreach (var data in finalData.Where(data => data.Speed != 0 || data.PackageLossPercentage != 0 || data.Ping != 0))
{
_dataTable.Rows.Add(data.Timestamp, data.PackageLoss, data.Ping);
_dataTable.Rows.Add(data.Timestamp, data.Speed, data.PackageLossPercentage, data.Ping);
}
StatisticsChart.DataBind();
}

private void serverSelector_SelectedIndexChanged(object sender, EventArgs e)
private void serverSelector_SelectionChangeCommitted(object sender, EventArgs e)
{
loadChartData();
LoadChartData();
}

private void chartModeSelector_Enter(object sender, EventArgs e)
private void dayMode_CheckedChanged(object sender, EventArgs e)
{
LoadChartData();
}

private void dayMode_CheckedChanged(object sender, EventArgs e)
private void allMode_CheckedChanged(object sender, EventArgs e)
{
loadChartData();
LoadChartData();
}

private void allMode_CheckedChanged(object sender, EventArgs e)
private void PingCheckBox_CheckedChanged(object sender, EventArgs e)
{
loadChartData();
repeatTimesNum.ReadOnly = !PingCheckBox.Checked;
}
}
}

+ 5
- 2
shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx View File

@@ -118,9 +118,12 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="bindingConfiguration.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1, 30</value>
<value>4, 5</value>
</metadata>
<metadata name="CalculatinTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>238, 6</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>63</value>
<value>191</value>
</metadata>
</root>

+ 10
- 9
shadowsocks-csharp/shadowsocks-csharp.csproj View File

@@ -60,6 +60,7 @@
<CodeAnalysisRuleSet>ManagedMinimumRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<DebugSymbols>true</DebugSymbols>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
@@ -195,6 +196,7 @@
<Compile Include="Model\LogViewerConfig.cs" />
<Compile Include="Model\Server.cs" />
<Compile Include="Model\Configuration.cs" />
<Compile Include="Model\StatisticsRecord.cs" />
<Compile Include="Model\StatisticsStrategyConfiguration.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
@@ -340,13 +342,12 @@
<Files Output="false" Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" />
</ParameterGroup>
<Task Evaluate="true">
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml" />
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml.Linq" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.IO" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.Xml.Linq" />
<Code xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Type="Fragment" Language="cs">
<![CDATA[
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml" />
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml.Linq" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.IO" />
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.Xml.Linq" />
<Code xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Type="Fragment" Language="cs"><![CDATA[
var config = XElement.Load(Config.ItemSpec).Elements("Costura").FirstOrDefault();

if (config == null) return true;
@@ -365,8 +366,8 @@ var filesToCleanup = Files.Select(f => f.ItemSpec).Where(f => !excludedAssemblie

foreach (var item in filesToCleanup)
File.Delete(item);
]]>
</Code></Task>
]]></Code>
</Task>
</UsingTask>
<Target Name="CleanReferenceCopyLocalPaths" AfterTargets="AfterBuild;NonWinFodyTarget">
<CosturaCleanup Config="FodyWeavers.xml" Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" />


Loading…
Cancel
Save