Add availability statistics service and a simple strategytags/2.5.3
@@ -0,0 +1,109 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Net.NetworkInformation; | |||
using System.Threading; | |||
using Shadowsocks.Model; | |||
using System.Reflection; | |||
namespace Shadowsocks.Controller | |||
{ | |||
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<Server> servers; | |||
public static string AvailabilityStatisticsFile; | |||
//static constructor to initialize every public static fields before refereced | |||
static AvailabilityStatistics() | |||
{ | |||
string temppath = Path.GetTempPath(); | |||
AvailabilityStatisticsFile = Path.Combine(temppath, StatisticsFilesName); | |||
} | |||
public bool Set(bool enabled) | |||
{ | |||
try | |||
{ | |||
if (enabled) | |||
{ | |||
if (timer?.Change(0, Interval) == null) | |||
{ | |||
state = new State(); | |||
timer = new Timer(Evaluate, state, 0, Interval); | |||
} | |||
} | |||
else | |||
{ | |||
timer?.Dispose(); | |||
} | |||
return true; | |||
} | |||
catch (Exception e) | |||
{ | |||
Logging.LogUsefulException(e); | |||
return false; | |||
} | |||
} | |||
private void Evaluate(object obj) | |||
{ | |||
Ping ping = new Ping(); | |||
State state = (State) obj; | |||
foreach (var server in servers) | |||
{ | |||
Logging.Debug("eveluating " + server.FriendlyName()); | |||
foreach (var _ in Enumerable.Range(0, Repeat)) | |||
{ | |||
//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 | |||
PingReply reply = ping.Send(server.server, Timeout); | |||
state.data = new List<KeyValuePair<string, string>>(); | |||
state.data.Add(new KeyValuePair<string, string>("Timestamp", timestamp)); | |||
state.data.Add(new KeyValuePair<string, string>("Server", server.FriendlyName())); | |||
state.data.Add(new KeyValuePair<string, string>("Status", reply.Status.ToString())); | |||
state.data.Add(new KeyValuePair<string, string>("RoundtripTime", reply.RoundtripTime.ToString())); | |||
//state.data.Add(new KeyValuePair<string, string>("data", reply.Buffer.ToString())); // The data of reply | |||
Append(state.data); | |||
} | |||
} | |||
} | |||
private static void Append(List<KeyValuePair<string, string>> data) | |||
{ | |||
string 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 }; | |||
} | |||
else | |||
{ | |||
lines = new string[] { dataLine }; | |||
} | |||
File.AppendAllLines(AvailabilityStatisticsFile, lines); | |||
} | |||
internal void UpdateConfiguration(Configuration _config) | |||
{ | |||
Set(_config.availabilityStatistics); | |||
servers = _config.configs; | |||
} | |||
private class State | |||
{ | |||
public List<KeyValuePair<string, string>> data = new List<KeyValuePair<string, string>>(); | |||
} | |||
} | |||
} |
@@ -25,6 +25,7 @@ namespace Shadowsocks.Controller | |||
private StrategyManager _strategyManager; | |||
private PolipoRunner polipoRunner; | |||
private GFWListUpdater gfwListUpdater; | |||
private AvailabilityStatistics _availabilityStatics; | |||
private bool stopped = false; | |||
private bool _systemProxyIsDirty = false; | |||
@@ -246,6 +247,16 @@ namespace Shadowsocks.Controller | |||
} | |||
} | |||
public void ToggleAvailabilityStatistics(bool enabled) | |||
{ | |||
if (_availabilityStatics != null) | |||
{ | |||
_availabilityStatics.Set(enabled); | |||
_config.availabilityStatistics = enabled; | |||
SaveConfig(_config); | |||
} | |||
} | |||
public void SavePACUrl(string pacUrl) | |||
{ | |||
_config.pacUrl = pacUrl; | |||
@@ -295,6 +306,12 @@ namespace Shadowsocks.Controller | |||
_listener.Stop(); | |||
} | |||
if (_availabilityStatics == null) | |||
{ | |||
_availabilityStatics = new AvailabilityStatistics(); | |||
_availabilityStatics.UpdateConfiguration(_config); | |||
} | |||
// 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 | |||
@@ -0,0 +1,176 @@ | |||
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<string, StatisticsData> 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<Server> 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<Server> 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 | |||
} | |||
} | |||
} |
@@ -13,6 +13,7 @@ namespace Shadowsocks.Controller.Strategy | |||
_strategies = new List<IStrategy>(); | |||
_strategies.Add(new BalancingStrategy(controller)); | |||
_strategies.Add(new HighAvailabilityStrategy(controller)); | |||
_strategies.Add(new SimplyChooseByStatisticsStrategy(controller)); | |||
// TODO: load DLL plugins | |||
} | |||
public IList<IStrategy> GetStrategies() | |||
@@ -25,6 +25,7 @@ Quit=退出 | |||
Edit Servers=编辑服务器 | |||
Load Balance=负载均衡 | |||
High Availability=高可用 | |||
Choose By Total Package Loss=累计丢包率 | |||
# Config Form | |||
@@ -22,6 +22,7 @@ namespace Shadowsocks.Model | |||
public int localPort; | |||
public string pacUrl; | |||
public bool useOnlinePac; | |||
public bool availabilityStatistics; | |||
private static string CONFIG_FILE = "gui-config.json"; | |||
@@ -29,6 +29,7 @@ namespace Shadowsocks.View | |||
private MenuItem enableItem; | |||
private MenuItem modeItem; | |||
private MenuItem AutoStartupItem; | |||
private MenuItem AvailabilityStatistics; | |||
private MenuItem ShareOverLANItem; | |||
private MenuItem SeperatorItem; | |||
private MenuItem ConfigItem; | |||
@@ -177,6 +178,7 @@ 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)), | |||
@@ -260,6 +262,7 @@ 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(); | |||
@@ -524,6 +527,11 @@ 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) | |||
@@ -123,7 +123,9 @@ | |||
<Compile Include="3rd\zxing\ResultPoint.cs" /> | |||
<Compile Include="3rd\zxing\ResultPointCallback.cs" /> | |||
<Compile Include="3rd\zxing\WriterException.cs" /> | |||
<Compile Include="Controller\Service\AvailabilityStatistics.cs" /> | |||
<Compile Include="Controller\Strategy\HighAvailabilityStrategy.cs" /> | |||
<Compile Include="Controller\Strategy\SimplyChooseByStatisticsStrategy.cs" /> | |||
<Compile Include="Controller\System\AutoStartup.cs" /> | |||
<Compile Include="Controller\FileManager.cs" /> | |||
<Compile Include="Controller\Service\GFWListUpdater.cs" /> | |||