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 StrategyManager _strategyManager; | ||||
private PolipoRunner polipoRunner; | private PolipoRunner polipoRunner; | ||||
private GFWListUpdater gfwListUpdater; | private GFWListUpdater gfwListUpdater; | ||||
private AvailabilityStatistics _availabilityStatics; | |||||
private bool stopped = false; | private bool stopped = false; | ||||
private bool _systemProxyIsDirty = 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) | public void SavePACUrl(string pacUrl) | ||||
{ | { | ||||
_config.pacUrl = pacUrl; | _config.pacUrl = pacUrl; | ||||
@@ -295,6 +306,12 @@ namespace Shadowsocks.Controller | |||||
_listener.Stop(); | _listener.Stop(); | ||||
} | } | ||||
if (_availabilityStatics == null) | |||||
{ | |||||
_availabilityStatics = new AvailabilityStatistics(); | |||||
_availabilityStatics.UpdateConfiguration(_config); | |||||
} | |||||
// don't put polipoRunner.Start() before pacServer.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 | // 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 | // 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 = new List<IStrategy>(); | ||||
_strategies.Add(new BalancingStrategy(controller)); | _strategies.Add(new BalancingStrategy(controller)); | ||||
_strategies.Add(new HighAvailabilityStrategy(controller)); | _strategies.Add(new HighAvailabilityStrategy(controller)); | ||||
_strategies.Add(new SimplyChooseByStatisticsStrategy(controller)); | |||||
// TODO: load DLL plugins | // TODO: load DLL plugins | ||||
} | } | ||||
public IList<IStrategy> GetStrategies() | public IList<IStrategy> GetStrategies() | ||||
@@ -25,6 +25,7 @@ Quit=退出 | |||||
Edit Servers=编辑服务器 | Edit Servers=编辑服务器 | ||||
Load Balance=负载均衡 | Load Balance=负载均衡 | ||||
High Availability=高可用 | High Availability=高可用 | ||||
Choose By Total Package Loss=累计丢包率 | |||||
# Config Form | # Config Form | ||||
@@ -22,6 +22,7 @@ namespace Shadowsocks.Model | |||||
public int localPort; | public int localPort; | ||||
public string pacUrl; | public string pacUrl; | ||||
public bool useOnlinePac; | public bool useOnlinePac; | ||||
public bool availabilityStatistics; | |||||
private static string CONFIG_FILE = "gui-config.json"; | private static string CONFIG_FILE = "gui-config.json"; | ||||
@@ -29,6 +29,7 @@ namespace Shadowsocks.View | |||||
private MenuItem enableItem; | private MenuItem enableItem; | ||||
private MenuItem modeItem; | private MenuItem modeItem; | ||||
private MenuItem AutoStartupItem; | private MenuItem AutoStartupItem; | ||||
private MenuItem AvailabilityStatistics; | |||||
private MenuItem ShareOverLANItem; | private MenuItem ShareOverLANItem; | ||||
private MenuItem SeperatorItem; | private MenuItem SeperatorItem; | ||||
private MenuItem ConfigItem; | private MenuItem ConfigItem; | ||||
@@ -177,6 +178,7 @@ namespace Shadowsocks.View | |||||
}), | }), | ||||
new MenuItem("-"), | new MenuItem("-"), | ||||
this.AutoStartupItem = CreateMenuItem("Start on Boot", new EventHandler(this.AutoStartupItem_Click)), | 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)), | this.ShareOverLANItem = CreateMenuItem("Allow Clients from LAN", new EventHandler(this.ShareOverLANItem_Click)), | ||||
new MenuItem("-"), | new MenuItem("-"), | ||||
CreateMenuItem("Show Logs...", new EventHandler(this.ShowLogItem_Click)), | CreateMenuItem("Show Logs...", new EventHandler(this.ShowLogItem_Click)), | ||||
@@ -260,6 +262,7 @@ namespace Shadowsocks.View | |||||
PACModeItem.Checked = !config.global; | PACModeItem.Checked = !config.global; | ||||
ShareOverLANItem.Checked = config.shareOverLan; | ShareOverLANItem.Checked = config.shareOverLan; | ||||
AutoStartupItem.Checked = AutoStartup.Check(); | AutoStartupItem.Checked = AutoStartup.Check(); | ||||
AvailabilityStatistics.Checked = config.availabilityStatistics; | |||||
onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; | onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; | ||||
localPACItem.Checked = !onlinePACItem.Checked; | localPACItem.Checked = !onlinePACItem.Checked; | ||||
UpdatePACItemsEnabledStatus(); | 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) | private void LocalPACItem_Click(object sender, EventArgs e) | ||||
{ | { | ||||
if (!localPACItem.Checked) | if (!localPACItem.Checked) | ||||
@@ -123,7 +123,9 @@ | |||||
<Compile Include="3rd\zxing\ResultPoint.cs" /> | <Compile Include="3rd\zxing\ResultPoint.cs" /> | ||||
<Compile Include="3rd\zxing\ResultPointCallback.cs" /> | <Compile Include="3rd\zxing\ResultPointCallback.cs" /> | ||||
<Compile Include="3rd\zxing\WriterException.cs" /> | <Compile Include="3rd\zxing\WriterException.cs" /> | ||||
<Compile Include="Controller\Service\AvailabilityStatistics.cs" /> | |||||
<Compile Include="Controller\Strategy\HighAvailabilityStrategy.cs" /> | <Compile Include="Controller\Strategy\HighAvailabilityStrategy.cs" /> | ||||
<Compile Include="Controller\Strategy\SimplyChooseByStatisticsStrategy.cs" /> | |||||
<Compile Include="Controller\System\AutoStartup.cs" /> | <Compile Include="Controller\System\AutoStartup.cs" /> | ||||
<Compile Include="Controller\FileManager.cs" /> | <Compile Include="Controller\FileManager.cs" /> | ||||
<Compile Include="Controller\Service\GFWListUpdater.cs" /> | <Compile Include="Controller\Service\GFWListUpdater.cs" /> | ||||