- Add an option for checking updates - download updates automaticallytags/3.0
@@ -1,137 +1,212 @@ | |||||
using Shadowsocks.Model; | |||||
using System; | |||||
using System; | |||||
using System.Collections; | using System.Collections; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Net; | using System.Net; | ||||
using System.Reflection; | using System.Reflection; | ||||
using System.Text; | using System.Text; | ||||
using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||
using System.IO; | |||||
using SimpleJson; | using SimpleJson; | ||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util; | |||||
namespace Shadowsocks.Controller | namespace Shadowsocks.Controller | ||||
{ | { | ||||
public class UpdateChecker | public class UpdateChecker | ||||
{ | { | ||||
private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; | private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; | ||||
private const string UserAgent = "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36"; | |||||
private Configuration config; | |||||
public bool NewVersionFound; | |||||
public string LatestVersionNumber; | public string LatestVersionNumber; | ||||
public string LatestVersionName; | |||||
public string LatestVersionURL; | public string LatestVersionURL; | ||||
public event EventHandler NewVersionFound; | |||||
public string LatestVersionLocalName; | |||||
public event EventHandler CheckUpdateCompleted; | |||||
public const string Version = "2.5.8"; | public const string Version = "2.5.8"; | ||||
public void CheckUpdate(Configuration config) | public void CheckUpdate(Configuration config) | ||||
{ | { | ||||
// TODO test failures | |||||
WebClient http = new WebClient(); | |||||
http.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36"); | |||||
http.Proxy = new WebProxy(IPAddress.Loopback.ToString(), config.localPort); | |||||
http.DownloadStringCompleted += http_DownloadStringCompleted; | |||||
http.DownloadStringAsync(new Uri(UpdateURL)); | |||||
this.config = config; | |||||
try | |||||
{ | |||||
WebClient http = CreateWebClient(); | |||||
http.DownloadStringCompleted += http_DownloadStringCompleted; | |||||
http.DownloadStringAsync(new Uri(UpdateURL)); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logging.LogUsefulException(ex); | |||||
} | |||||
} | } | ||||
public static int CompareVersion(string l, string r) | |||||
private void http_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) | |||||
{ | { | ||||
var ls = l.Split('.'); | |||||
var rs = r.Split('.'); | |||||
for (int i = 0; i < Math.Max(ls.Length, rs.Length); i++) | |||||
try | |||||
{ | { | ||||
int lp = (i < ls.Length) ? int.Parse(ls[i]) : 0; | |||||
int rp = (i < rs.Length) ? int.Parse(rs[i]) : 0; | |||||
if (lp != rp) | |||||
string response = e.Result; | |||||
JsonArray result = (JsonArray)SimpleJson.SimpleJson.DeserializeObject(e.Result); | |||||
List<Asset> asserts = new List<Asset>(); | |||||
foreach (JsonObject release in result) | |||||
{ | |||||
if ((bool)release["prerelease"]) | |||||
{ | |||||
continue; | |||||
} | |||||
foreach (JsonObject asset in (JsonArray)release["assets"]) | |||||
{ | |||||
Asset ass = new Asset(); | |||||
ass.Parse(asset); | |||||
if (ass.IsNewVersion(Version)) | |||||
{ | |||||
asserts.Add(ass); | |||||
} | |||||
} | |||||
} | |||||
if (asserts.Count != 0) | |||||
{ | { | ||||
return lp - rp; | |||||
SortByVersions(asserts); | |||||
Asset asset = asserts[asserts.Count - 1]; | |||||
NewVersionFound = true; | |||||
LatestVersionURL = asset.browser_download_url; | |||||
LatestVersionNumber = asset.version; | |||||
LatestVersionName = asset.name; | |||||
startDownload(); | |||||
} | } | ||||
else if (CheckUpdateCompleted != null) | |||||
{ | |||||
CheckUpdateCompleted(this, new EventArgs()); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logging.LogUsefulException(ex); | |||||
} | } | ||||
return 0; | |||||
} | } | ||||
public class VersionComparer : IComparer<string> | |||||
private void startDownload() | |||||
{ | { | ||||
// Calls CaseInsensitiveComparer.Compare with the parameters reversed. | |||||
public int Compare(string x, string y) | |||||
try | |||||
{ | { | ||||
return CompareVersion(ParseVersionFromURL(x), ParseVersionFromURL(y)); | |||||
string temppath = Utils.GetTempPath(); | |||||
LatestVersionLocalName = Path.Combine(temppath, LatestVersionName); | |||||
WebClient http = CreateWebClient(); | |||||
http.DownloadFileCompleted += Http_DownloadFileCompleted; | |||||
http.DownloadFileAsync(new Uri(LatestVersionURL), LatestVersionLocalName); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logging.LogUsefulException(ex); | |||||
} | } | ||||
} | } | ||||
private static string ParseVersionFromURL(string url) | |||||
private void Http_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) | |||||
{ | { | ||||
Match match = Regex.Match(url, @".*Shadowsocks-win.*?-([\d\.]+)\.\w+", RegexOptions.IgnoreCase); | |||||
if (match.Success) | |||||
try | |||||
{ | { | ||||
if (match.Groups.Count == 2) | |||||
if(e.Error != null) | |||||
{ | |||||
Logging.LogUsefulException(e.Error); | |||||
return; | |||||
} | |||||
if (CheckUpdateCompleted != null) | |||||
{ | { | ||||
return match.Groups[1].Value; | |||||
CheckUpdateCompleted(this, new EventArgs()); | |||||
} | } | ||||
} | } | ||||
return null; | |||||
catch (Exception ex) | |||||
{ | |||||
Logging.LogUsefulException(ex); | |||||
} | |||||
} | } | ||||
private void SortVersions(List<string> versions) | |||||
private WebClient CreateWebClient() | |||||
{ | { | ||||
versions.Sort(new VersionComparer()); | |||||
WebClient http = new WebClient(); | |||||
http.Headers.Add("User-Agent", UserAgent); | |||||
http.Proxy = new WebProxy(IPAddress.Loopback.ToString(), config.localPort); | |||||
return http; | |||||
} | } | ||||
private bool IsNewVersion(string url) | |||||
private void SortByVersions(List<Asset> asserts) | |||||
{ | { | ||||
if (url.IndexOf("prerelease") >= 0) | |||||
{ | |||||
return false; | |||||
} | |||||
string version = ParseVersionFromURL(url); | |||||
if (version == null) | |||||
{ | |||||
return false; | |||||
} | |||||
string currentVersion = Version; | |||||
return CompareVersion(version, currentVersion) > 0; | |||||
asserts.Sort(new VersionComparer()); | |||||
} | } | ||||
private void http_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) | |||||
public class Asset | |||||
{ | { | ||||
try | |||||
public bool prerelease; | |||||
public string name; | |||||
public string version; | |||||
public string browser_download_url; | |||||
public bool IsNewVersion(string currentVersion) | |||||
{ | { | ||||
string response = e.Result; | |||||
if (prerelease) | |||||
{ | |||||
return false; | |||||
} | |||||
if (version == null) | |||||
{ | |||||
return false; | |||||
} | |||||
return CompareVersion(version, currentVersion) > 0; | |||||
} | |||||
JsonArray result = (JsonArray)SimpleJson.SimpleJson.DeserializeObject(e.Result); | |||||
public void Parse(JsonObject asset) | |||||
{ | |||||
name = (string)asset["name"]; | |||||
browser_download_url = (string)asset["browser_download_url"]; | |||||
version = ParseVersionFromURL(browser_download_url); | |||||
prerelease = browser_download_url.IndexOf("prerelease") >= 0; | |||||
} | |||||
List<string> versions = new List<string>(); | |||||
foreach (JsonObject release in result) | |||||
private static string ParseVersionFromURL(string url) | |||||
{ | |||||
Match match = Regex.Match(url, @".*Shadowsocks-win.*?-([\d\.]+)\.\w+", RegexOptions.IgnoreCase); | |||||
if (match.Success) | |||||
{ | { | ||||
if ((bool)release["prerelease"]) | |||||
if (match.Groups.Count == 2) | |||||
{ | { | ||||
continue; | |||||
} | |||||
foreach (JsonObject asset in (JsonArray)release["assets"]) | |||||
{ | |||||
string url = (string)asset["browser_download_url"]; | |||||
if (IsNewVersion(url)) | |||||
{ | |||||
versions.Add(url); | |||||
} | |||||
return match.Groups[1].Value; | |||||
} | } | ||||
} | } | ||||
return null; | |||||
} | |||||
if (versions.Count == 0) | |||||
{ | |||||
return; | |||||
} | |||||
// sort versions | |||||
SortVersions(versions); | |||||
LatestVersionURL = versions[versions.Count - 1]; | |||||
LatestVersionNumber = ParseVersionFromURL(LatestVersionURL); | |||||
if (NewVersionFound != null) | |||||
public static int CompareVersion(string l, string r) | |||||
{ | |||||
var ls = l.Split('.'); | |||||
var rs = r.Split('.'); | |||||
for (int i = 0; i < Math.Max(ls.Length, rs.Length); i++) | |||||
{ | { | ||||
NewVersionFound(this, new EventArgs()); | |||||
int lp = (i < ls.Length) ? int.Parse(ls[i]) : 0; | |||||
int rp = (i < rs.Length) ? int.Parse(rs[i]) : 0; | |||||
if (lp != rp) | |||||
{ | |||||
return lp - rp; | |||||
} | |||||
} | } | ||||
return 0; | |||||
} | } | ||||
catch (Exception ex) | |||||
} | |||||
class VersionComparer : IComparer<Asset> | |||||
{ | |||||
// Calls CaseInsensitiveComparer.Compare with the parameters reversed. | |||||
public int Compare(Asset x, Asset y) | |||||
{ | { | ||||
Logging.Debug(ex.ToString()); | |||||
return; | |||||
return Asset.CompareVersion(x.version, y.version); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } |
@@ -280,6 +280,12 @@ namespace Shadowsocks.Controller | |||||
} | } | ||||
} | } | ||||
public void ToggleCheckingUpdate(bool enabled) | |||||
{ | |||||
_config.autoCheckUpdate = enabled; | |||||
Configuration.Save(_config); | |||||
} | |||||
protected void Reload() | protected void Reload() | ||||
{ | { | ||||
// some logic in configuration updated the config when saving, we need to read it again | // some logic in configuration updated the config when saving, we need to read it again | ||||
@@ -21,6 +21,9 @@ Show QRCode...=显示二维码... | |||||
Scan QRCode from Screen...=扫描屏幕上的二维码... | Scan QRCode from Screen...=扫描屏幕上的二维码... | ||||
Availability Statistics=统计可用性 | Availability Statistics=统计可用性 | ||||
Show Logs...=显示日志... | Show Logs...=显示日志... | ||||
Updates...=更新... | |||||
Check Updates...=检查更新 | |||||
Automatically Check Updates=自动检查更新 | |||||
About...=关于... | About...=关于... | ||||
Quit=退出 | Quit=退出 | ||||
Edit Servers=编辑服务器 | Edit Servers=编辑服务器 | ||||
@@ -78,7 +81,8 @@ Password can not be blank=密码不能为空 | |||||
Port out of range=端口超出范围 | Port out of range=端口超出范围 | ||||
Port can't be 8123=端口不能为 8123 | Port can't be 8123=端口不能为 8123 | ||||
Shadowsocks {0} Update Found=Shadowsocks {0} 更新 | Shadowsocks {0} Update Found=Shadowsocks {0} 更新 | ||||
Click here to download=点击这里下载 | |||||
No update is available=没有可用的更新 | |||||
Click here to update=点击这里升级 | |||||
Shadowsocks is here=Shadowsocks 在这里 | Shadowsocks is here=Shadowsocks 在这里 | ||||
You can turn on/off Shadowsocks in the context menu=可以在右键菜单中开关 Shadowsocks | You can turn on/off Shadowsocks in the context menu=可以在右键菜单中开关 Shadowsocks | ||||
System Proxy Enabled=系统代理已启用 | System Proxy Enabled=系统代理已启用 | ||||
@@ -23,6 +23,7 @@ namespace Shadowsocks.Model | |||||
public string pacUrl; | public string pacUrl; | ||||
public bool useOnlinePac; | public bool useOnlinePac; | ||||
public bool availabilityStatistics; | public bool availabilityStatistics; | ||||
public bool autoCheckUpdate; | |||||
private static string CONFIG_FILE = "gui-config.json"; | private static string CONFIG_FILE = "gui-config.json"; | ||||
@@ -76,6 +77,7 @@ namespace Shadowsocks.Model | |||||
index = 0, | index = 0, | ||||
isDefault = true, | isDefault = true, | ||||
localPort = 1080, | localPort = 1080, | ||||
autoCheckUpdate = true, | |||||
configs = new List<Server>() | configs = new List<Server>() | ||||
{ | { | ||||
GetDefaultServer() | GetDefaultServer() | ||||
@@ -26,6 +26,7 @@ namespace Shadowsocks.View | |||||
private ContextMenu contextMenu1; | private ContextMenu contextMenu1; | ||||
private bool _isFirstRun; | private bool _isFirstRun; | ||||
private bool _isStartupChecking; | |||||
private MenuItem enableItem; | private MenuItem enableItem; | ||||
private MenuItem modeItem; | private MenuItem modeItem; | ||||
private MenuItem AutoStartupItem; | private MenuItem AutoStartupItem; | ||||
@@ -42,6 +43,7 @@ namespace Shadowsocks.View | |||||
private MenuItem updateFromGFWListItem; | private MenuItem updateFromGFWListItem; | ||||
private MenuItem editGFWUserRuleItem; | private MenuItem editGFWUserRuleItem; | ||||
private MenuItem editOnlinePACItem; | private MenuItem editOnlinePACItem; | ||||
private MenuItem autoCheckUpdatesToggleItem; | |||||
private ConfigForm configForm; | private ConfigForm configForm; | ||||
private string _urlToOpen; | private string _urlToOpen; | ||||
@@ -68,13 +70,19 @@ namespace Shadowsocks.View | |||||
_notifyIcon.MouseDoubleClick += notifyIcon1_DoubleClick; | _notifyIcon.MouseDoubleClick += notifyIcon1_DoubleClick; | ||||
this.updateChecker = new UpdateChecker(); | this.updateChecker = new UpdateChecker(); | ||||
updateChecker.NewVersionFound += updateChecker_NewVersionFound; | |||||
updateChecker.CheckUpdateCompleted += updateChecker_CheckUpdateCompleted; | |||||
LoadCurrentConfiguration(); | LoadCurrentConfiguration(); | ||||
updateChecker.CheckUpdate(controller.GetConfigurationCopy()); | |||||
Configuration config = controller.GetConfigurationCopy(); | |||||
if (controller.GetConfigurationCopy().isDefault) | |||||
if (config.autoCheckUpdate) | |||||
{ | |||||
_isStartupChecking = true; | |||||
updateChecker.CheckUpdate(config); | |||||
} | |||||
if (config.isDefault) | |||||
{ | { | ||||
_isFirstRun = true; | _isFirstRun = true; | ||||
ShowConfigForm(); | ShowConfigForm(); | ||||
@@ -182,6 +190,11 @@ namespace Shadowsocks.View | |||||
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)), | ||||
CreateMenuGroup("Updates...", new MenuItem[] { | |||||
CreateMenuItem("Check Updates...", new EventHandler(this.checkUpdatesItem_Click)), | |||||
new MenuItem("-"), | |||||
this.autoCheckUpdatesToggleItem = CreateMenuItem("Automatically Check Updates", new EventHandler(this.autoCheckUpdatesToggleItem_Click)), | |||||
}), | |||||
CreateMenuItem("About...", new EventHandler(this.AboutItem_Click)), | CreateMenuItem("About...", new EventHandler(this.AboutItem_Click)), | ||||
new MenuItem("-"), | new MenuItem("-"), | ||||
CreateMenuItem("Quit", new EventHandler(this.Quit_Click)) | CreateMenuItem("Quit", new EventHandler(this.Quit_Click)) | ||||
@@ -238,17 +251,27 @@ namespace Shadowsocks.View | |||||
ShowBalloonTip(I18N.GetString("Shadowsocks"), result, ToolTipIcon.Info, 1000); | ShowBalloonTip(I18N.GetString("Shadowsocks"), result, ToolTipIcon.Info, 1000); | ||||
} | } | ||||
void updateChecker_NewVersionFound(object sender, EventArgs e) | |||||
void updateChecker_CheckUpdateCompleted(object sender, EventArgs e) | |||||
{ | { | ||||
ShowBalloonTip(String.Format(I18N.GetString("Shadowsocks {0} Update Found"), updateChecker.LatestVersionNumber), I18N.GetString("Click here to download"), ToolTipIcon.Info, 5000); | |||||
_notifyIcon.BalloonTipClicked += notifyIcon1_BalloonTipClicked; | |||||
_isFirstRun = false; | |||||
if (updateChecker.NewVersionFound) | |||||
{ | |||||
ShowBalloonTip(String.Format(I18N.GetString("Shadowsocks {0} Update Found"), updateChecker.LatestVersionNumber), I18N.GetString("Click here to update"), ToolTipIcon.Info, 5000); | |||||
_notifyIcon.BalloonTipClicked += notifyIcon1_BalloonTipClicked; | |||||
_isFirstRun = false; | |||||
} | |||||
else if (!_isStartupChecking) | |||||
{ | |||||
ShowBalloonTip(I18N.GetString("Shadowsocks"), I18N.GetString("No update is available"), ToolTipIcon.Info, 5000); | |||||
_isFirstRun = false; | |||||
} | |||||
_isStartupChecking = false; | |||||
} | } | ||||
void notifyIcon1_BalloonTipClicked(object sender, EventArgs e) | void notifyIcon1_BalloonTipClicked(object sender, EventArgs e) | ||||
{ | { | ||||
System.Diagnostics.Process.Start(updateChecker.LatestVersionURL); | |||||
_notifyIcon.BalloonTipClicked -= notifyIcon1_BalloonTipClicked; | _notifyIcon.BalloonTipClicked -= notifyIcon1_BalloonTipClicked; | ||||
string argument = "/select, \"" + updateChecker.LatestVersionLocalName + "\""; | |||||
System.Diagnostics.Process.Start("explorer.exe", argument); | |||||
} | } | ||||
@@ -266,6 +289,7 @@ namespace Shadowsocks.View | |||||
onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; | onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; | ||||
localPACItem.Checked = !onlinePACItem.Checked; | localPACItem.Checked = !onlinePACItem.Checked; | ||||
UpdatePACItemsEnabledStatus(); | UpdatePACItemsEnabledStatus(); | ||||
UpdateUpdateMenu(); | |||||
} | } | ||||
private void UpdateServersMenu() | private void UpdateServersMenu() | ||||
@@ -343,7 +367,7 @@ namespace Shadowsocks.View | |||||
if (_isFirstRun) | if (_isFirstRun) | ||||
{ | { | ||||
_notifyIcon.BalloonTipTitle = I18N.GetString("Shadowsocks is here"); | _notifyIcon.BalloonTipTitle = I18N.GetString("Shadowsocks is here"); | ||||
_notifyIcon.BalloonTipText = I18N.GetString("You can turn on/off Shadowsocks in the context menu"); | |||||
_notifyIcon.BalloonTipText = I18N.GetString("You can turn on/off Shadowsocks in the context menu"); | |||||
_notifyIcon.BalloonTipIcon = ToolTipIcon.Info; | _notifyIcon.BalloonTipIcon = ToolTipIcon.Info; | ||||
_notifyIcon.ShowBalloonTip(0); | _notifyIcon.ShowBalloonTip(0); | ||||
_isFirstRun = false; | _isFirstRun = false; | ||||
@@ -591,5 +615,23 @@ namespace Shadowsocks.View | |||||
this.editOnlinePACItem.Enabled = true; | this.editOnlinePACItem.Enabled = true; | ||||
} | } | ||||
} | } | ||||
private void UpdateUpdateMenu() | |||||
{ | |||||
Configuration configuration = controller.GetConfigurationCopy(); | |||||
autoCheckUpdatesToggleItem.Checked = configuration.autoCheckUpdate; | |||||
} | |||||
private void autoCheckUpdatesToggleItem_Click(object sender, EventArgs e) | |||||
{ | |||||
Configuration configuration = controller.GetConfigurationCopy(); | |||||
controller.ToggleCheckingUpdate(!configuration.autoCheckUpdate); | |||||
UpdateUpdateMenu(); | |||||
} | |||||
private void checkUpdatesItem_Click(object sender, EventArgs e) | |||||
{ | |||||
updateChecker.CheckUpdate(controller.GetConfigurationCopy()); | |||||
} | |||||
} | } | ||||
} | } |
@@ -13,13 +13,13 @@ namespace test | |||||
[TestMethod] | [TestMethod] | ||||
public void TestCompareVersion() | public void TestCompareVersion() | ||||
{ | { | ||||
Assert.IsTrue(UpdateChecker.CompareVersion("2.3.1.0", "2.3.1") == 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("1.2", "1.3") < 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("1.3", "1.2") > 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("1.3", "1.3") == 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("1.2.1", "1.2") > 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("2.3.1", "2.4") < 0); | |||||
Assert.IsTrue(UpdateChecker.CompareVersion("1.3.2", "1.3.1") > 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("2.3.1.0", "2.3.1") == 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("1.2", "1.3") < 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("1.3", "1.2") > 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("1.3", "1.3") == 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("1.2.1", "1.2") > 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("2.3.1", "2.4") < 0); | |||||
Assert.IsTrue(UpdateChecker.Asset.CompareVersion("1.3.2", "1.3.1") > 0); | |||||
} | } | ||||
private void RunEncryptionRound(IEncryptor encryptor, IEncryptor decryptor) | private void RunEncryptionRound(IEncryptor encryptor, IEncryptor decryptor) | ||||