From db22a466eaf5a8d39bc6aa9286b0d9b8de3fc2b4 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:31:45 +0100 Subject: [PATCH 01/12] =?UTF-8?q?=E2=9E=95=20Add=20UpdateChannel=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VPUpdater/UpdateChannel.cs | 36 ++++++++++++++++++++++++++++++++++++ VPUpdater/VPUpdater.csproj | 2 ++ 2 files changed, 38 insertions(+) create mode 100644 VPUpdater/UpdateChannel.cs diff --git a/VPUpdater/UpdateChannel.cs b/VPUpdater/UpdateChannel.cs new file mode 100644 index 0000000..88404dd --- /dev/null +++ b/VPUpdater/UpdateChannel.cs @@ -0,0 +1,36 @@ +#region Copyright + +// ----------------------------------------------------------------------- +// +// (C) 2019 Oliver Booth. All rights reserved. +// +// ----------------------------------------------------------------------- + +#endregion + +namespace VPUpdater +{ + #region Using Directives + + using System.ComponentModel; + + #endregion + + /// + /// An enumeration of update channels. + /// + public enum UpdateChannel + { + /// + /// Stable releases. + /// + [Description("Stable releases.")] + Stable, + + /// + /// Pre-releases. + /// + [Description("Pre-releases.")] + PreRelease + } +} diff --git a/VPUpdater/VPUpdater.csproj b/VPUpdater/VPUpdater.csproj index cc0218b..2fe33d6 100644 --- a/VPUpdater/VPUpdater.csproj +++ b/VPUpdater/VPUpdater.csproj @@ -64,6 +64,7 @@ + ..\packages\System.Text.Encoding.CodePages.4.5.0\lib\net46\System.Text.Encoding.CodePages.dll @@ -78,6 +79,7 @@ + From ada39909ba0fad9bfb7924d8a6301ebcc1299235 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:35:57 +0100 Subject: [PATCH 02/12] =?UTF-8?q?=E2=9C=A8=20Utilise=20UpdateChannel=20enu?= =?UTF-8?q?m=20in=20fetching=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FetchLatest() scans Edwin's blog for Virtual Paradise post titles that match a Semantic Version regex if channel is set to PreRelease --- VPUpdater/Updater.cs | 64 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/VPUpdater/Updater.cs b/VPUpdater/Updater.cs index d9c424d..bb6b9ab 100644 --- a/VPUpdater/Updater.cs +++ b/VPUpdater/Updater.cs @@ -17,12 +17,12 @@ namespace VPUpdater using System.IO; using System.Linq; using System.Net; + using System.ServiceModel.Syndication; using System.Text.RegularExpressions; using System.Threading.Tasks; - using System.Windows.Forms; + using System.Xml; using AngleSharp; using AngleSharp.Dom; - using AngleSharp.Io; using Properties; using SemVer = SemVer.Version; @@ -35,6 +35,12 @@ public class Updater : IDisposable { #region Fields + /// + /// Regular expression for matching semantic version strings. + /// + private const string SemVerRegex = + @"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?"; + /// /// The instance. /// @@ -125,25 +131,58 @@ public async Task Download(Uri uri) /// Fetches the latest version of Virtual Paradise. /// /// Gets the version string of the latest stable Virtual Paradise. - public async Task FetchLatest() + public async Task FetchLatest(UpdateChannel channel = UpdateChannel.Stable) { using (WebClient client = new WebClient()) { - client.DownloadProgressChanged += this.WebClientProgressChanged; - this.webClient = client; - - Uri uri = new Uri(VirtualParadise.Uri, @"version.txt"); - string versionString = await client.DownloadStringTaskAsync(uri); - - return new SemVer(versionString); + switch (channel) + { + case UpdateChannel.PreRelease: + string rssUri = new Uri(VirtualParadise.Uri, @"/edwin/feed").ToString(); + using (XmlReader rssReader = XmlReader.Create(rssUri)) + { + SyndicationFeed feed = SyndicationFeed.Load(rssReader); + Match match = null; + SyndicationItem item = + feed.Items.FirstOrDefault(i => Regex.Match(i.Title.Text, @"Virtual Paradise").Success && + (match = + Regex.Match( + i.Title.Text, SemVerRegex, + RegexOptions.IgnoreCase)) + .Success); + + if (!(item is null || match is null)) + { + // Return the version + return new SemVer(match.Value); + } + + // Return the latest stable instead + return await this.FetchLatest(); + } + + case UpdateChannel.Stable: + default: + client.DownloadProgressChanged += this.WebClientProgressChanged; + this.webClient = client; + + Uri uri = new Uri(VirtualParadise.Uri, @"version.txt"); + string versionString = await client.DownloadStringTaskAsync(uri); + + return new SemVer(versionString); + } } } /// /// Fetches the Uri of the latest download from the Virtual Paradise download page. /// + /// The update channel to use. + /// If is set to , the method will parse + /// this for a valid download URI. /// Returns a containing the download link. - public async Task FetchDownloadLink() + public async Task FetchDownloadLink(UpdateChannel channel = UpdateChannel.Stable, + SyndicationItem item = null) { IConfiguration config = Configuration.Default.WithDefaultLoader(); IBrowsingContext context = BrowsingContext.New(config); @@ -173,7 +212,8 @@ public async Task Launch() { if (!File.Exists(this.setupPath)) { - throw new FileNotFoundException(String.Format(Resources.FileNotFound, Path.GetFileName(this.setupPath))); + throw new FileNotFoundException( + String.Format(Resources.FileNotFound, Path.GetFileName(this.setupPath))); } await Task.Run(() => From a6cbf274e0be1feb593ef4af15de8beada40be96 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:36:41 +0100 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=94=A8=20Call=20DownloadForm.CheckF?= =?UTF-8?q?orUpdates()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of calling Updater.FetchLatest() directly --- VPUpdater/DownloadForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VPUpdater/DownloadForm.cs b/VPUpdater/DownloadForm.cs index 6d79d71..0d22fe0 100644 --- a/VPUpdater/DownloadForm.cs +++ b/VPUpdater/DownloadForm.cs @@ -141,7 +141,7 @@ private async void Run() this.labelDownloading.Text = Resources.UpdateCheck; Version currentVersion = this.virtualParadise.Version; - Version latestVersion = await this.updater.FetchLatest(); + Version latestVersion = await this.CheckForUpdates(); if (currentVersion < latestVersion) { DialogResult result = MessageBox.Show( From ed317974d2d57e1759c02d6138f06bc923554fa6 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:46:48 +0100 Subject: [PATCH 04/12] =?UTF-8?q?=E2=AC=86=20Update=20project=20to=20.NET?= =?UTF-8?q?=20Framework=204.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++++----- VPUpdater/App.config | 6 +++--- VPUpdater/Properties/Settings.Designer.cs | 22 +++++++++------------- VPUpdater/VPUpdater.csproj | 3 ++- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 39b7636..871b964 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,18 @@ This tool is directly inspired by [VPUpdater](https://github.com/Evonex/VPUpdate I've rewritten the tool from the ground up in C# to correct all of these issues. ## How to use -1. Download the [latest release](https://github.com/oliverbooth/VPUpdater) and extract the files to your Virtual Paradise directory -2. Create a shortcut to VPUpdater.exe -3. Rename the shortcut to something meaningful (you know... like `Virtual Paradise`) -4. Place that shortcut on your desktop / taskbar / useful location. +1. Ensure you have the [.NET Framework 4.8 Runtime](https://dotnet.microsoft.com/download/dotnet-framework/net48) +2. Download the [latest release](https://github.com/oliverbooth/VPUpdater) and extract the files to your Virtual Paradise directory +3. Create a shortcut to VPUpdater.exe +4. Rename the shortcut to something meaningful (you know... like `Virtual Paradise`) +5. Place that shortcut on your desktop / taskbar / useful location. Now when you launch "Virtual Paradise", it will launch the updater first. If there are no updates, the tool with simply launch Virtual Paradise. If there is an update, it will download the latest setup and you can go from there. ## Building prerequisites |Prerequisite|Version| |- |- | -|.NET|4.6| +|[.NET Framework Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/net48)|4.8| ## Nuget package dependencies |Dependency|Version| diff --git a/VPUpdater/App.config b/VPUpdater/App.config index 8324aa6..4bfa005 100644 --- a/VPUpdater/App.config +++ b/VPUpdater/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/VPUpdater/Properties/Settings.Designer.cs b/VPUpdater/Properties/Settings.Designer.cs index d85c827..804a7c7 100644 --- a/VPUpdater/Properties/Settings.Designer.cs +++ b/VPUpdater/Properties/Settings.Designer.cs @@ -8,21 +8,17 @@ // //------------------------------------------------------------------------------ -namespace VPUpdater.Properties -{ - - +namespace VPUpdater.Properties { + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.2.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { + + public static Settings Default { + get { return defaultInstance; } } diff --git a/VPUpdater/VPUpdater.csproj b/VPUpdater/VPUpdater.csproj index 2fe33d6..7720dfc 100644 --- a/VPUpdater/VPUpdater.csproj +++ b/VPUpdater/VPUpdater.csproj @@ -8,10 +8,11 @@ WinExe VPUpdater VPUpdater - v4.6 + v4.8 512 true true + AnyCPU From 9c31ad8faa982fe2bb2f30e22c18ca8c0937d246 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:48:48 +0100 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=8E=A8=20Add=20#region=20for=20usin?= =?UTF-8?q?g=20directives=20in=20Program.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VPUpdater/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VPUpdater/Program.cs b/VPUpdater/Program.cs index 55bfe4a..4a59447 100644 --- a/VPUpdater/Program.cs +++ b/VPUpdater/Program.cs @@ -10,10 +10,14 @@ namespace VPUpdater { + #region Using Directives + using System; using System.Linq; using System.Windows.Forms; + #endregion + /// /// Application entry class. /// From b943c781aa0e1f3c231d59c47e49e97baeab6f1c Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 16:54:15 +0100 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=94=A8=20Restrict=20scope=20of=20We?= =?UTF-8?q?bClient=20to=20Stable=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VPUpdater/Updater.cs | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/VPUpdater/Updater.cs b/VPUpdater/Updater.cs index bb6b9ab..1be26f2 100644 --- a/VPUpdater/Updater.cs +++ b/VPUpdater/Updater.cs @@ -133,36 +133,36 @@ public async Task Download(Uri uri) /// Gets the version string of the latest stable Virtual Paradise. public async Task FetchLatest(UpdateChannel channel = UpdateChannel.Stable) { - using (WebClient client = new WebClient()) + switch (channel) { - switch (channel) - { - case UpdateChannel.PreRelease: - string rssUri = new Uri(VirtualParadise.Uri, @"/edwin/feed").ToString(); - using (XmlReader rssReader = XmlReader.Create(rssUri)) + case UpdateChannel.PreRelease: + string rssUri = new Uri(VirtualParadise.Uri, @"/edwin/feed").ToString(); + using (XmlReader rssReader = XmlReader.Create(rssUri)) + { + SyndicationFeed feed = SyndicationFeed.Load(rssReader); + Match match = null; + SyndicationItem item = + feed.Items.FirstOrDefault(i => Regex.Match(i.Title.Text, @"Virtual Paradise").Success && + (match = + Regex.Match( + i.Title.Text, SemVerRegex, + RegexOptions.IgnoreCase)) + .Success); + + if (!(item is null || match is null)) { - SyndicationFeed feed = SyndicationFeed.Load(rssReader); - Match match = null; - SyndicationItem item = - feed.Items.FirstOrDefault(i => Regex.Match(i.Title.Text, @"Virtual Paradise").Success && - (match = - Regex.Match( - i.Title.Text, SemVerRegex, - RegexOptions.IgnoreCase)) - .Success); - - if (!(item is null || match is null)) - { - // Return the version - return new SemVer(match.Value); - } - - // Return the latest stable instead - return await this.FetchLatest(); + // Return the version + return new SemVer(match.Value); } - case UpdateChannel.Stable: - default: + // Return the latest stable instead + return await this.FetchLatest(); + } + + case UpdateChannel.Stable: + default: + using (WebClient client = new WebClient()) + { client.DownloadProgressChanged += this.WebClientProgressChanged; this.webClient = client; @@ -170,7 +170,7 @@ public async Task FetchLatest(UpdateChannel channel = UpdateChannel.Stab string versionString = await client.DownloadStringTaskAsync(uri); return new SemVer(versionString); - } + } } } From c84632d65c131708c9255b77fe841c3abb323332 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 17:54:16 +0100 Subject: [PATCH 07/12] =?UTF-8?q?=E2=9E=95=20Add=20UpdaterConfig.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used to (de)serialize VPUpdater.cfg which will store app configuration. Format is the same as virtualparadise.cfg with each line being key=value --- VPUpdater/UpdaterConfig.cs | 143 +++++++++++++++++++++++++++++++++++++ VPUpdater/VPUpdater.csproj | 1 + 2 files changed, 144 insertions(+) create mode 100644 VPUpdater/UpdaterConfig.cs diff --git a/VPUpdater/UpdaterConfig.cs b/VPUpdater/UpdaterConfig.cs new file mode 100644 index 0000000..e8f9429 --- /dev/null +++ b/VPUpdater/UpdaterConfig.cs @@ -0,0 +1,143 @@ +#region Copyright + +// ----------------------------------------------------------------------- +// +// (C) 2019 Oliver Booth. All rights reserved. +// +// ----------------------------------------------------------------------- + +#endregion + +namespace VPUpdater +{ + #region Using Directives + + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Threading.Tasks; + + #endregion + + /// + /// Represents a serialized version of the file VPUpdater.cfg + /// + [Serializable] + public class UpdaterConfig + { + #region Fields + + /// + /// The filename of the configuration. + /// + private const string ConfigFile = @"VPUpdater.cfg"; + + /// + /// Config dictionary. + /// + private readonly Dictionary configValues; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The config dictionary. + private UpdaterConfig(Dictionary configValues) + { + this.configValues = configValues; + } + + #endregion + + #region Properties + + /// + /// Gets or sets a config value. + /// + /// The key to fetch. + /// The default value to return, should not be found. + /// Returns the value. + public object this[string key, object defaultValue = default] + { + get => this.configValues.ContainsKey(key) + ? Convert.ChangeType(this.configValues[key], defaultValue?.GetType() ?? typeof(object)) + : defaultValue; + set + { + if (this.configValues.ContainsKey(key)) + { + this.configValues[key] = value; + } + else + { + this.configValues.Add(key, value); + } + } + } + + #endregion + + #region Methods + + /// + /// Reads the configuration file and returns an instance of that represents it. + /// + /// Returns an instance of . + public static async Task Load() + { + if (!File.Exists(ConfigFile)) + { + return new UpdaterConfig(new Dictionary()); + } + + using (StreamReader reader = new StreamReader(ConfigFile, Encoding.UTF8)) + { + Dictionary configValues = new Dictionary(); + string contents = await reader.ReadToEndAsync(); + string[] lines = contents.Split(Environment.NewLine.ToCharArray()); + + foreach (string line in lines) + { + if (String.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] split = line.Split(new[] {'='}, 2); + + if (configValues.ContainsKey(split[0])) + { + configValues[split[0]] = split[1]; + } + else + { + configValues.Add(split[0], split[1]); + } + } + + UpdaterConfig config = new UpdaterConfig(configValues); + return config; + } + } + + /// + /// Saves the configuration to file. + /// + public async Task Save() + { + using (StreamWriter writer = new StreamWriter(ConfigFile, false, Encoding.UTF8)) + { + foreach (KeyValuePair pair in this.configValues) + { + await writer.WriteLineAsync($"{pair.Key}={pair.Value}"); + } + } + } + + #endregion + } +} diff --git a/VPUpdater/VPUpdater.csproj b/VPUpdater/VPUpdater.csproj index 7720dfc..29895ae 100644 --- a/VPUpdater/VPUpdater.csproj +++ b/VPUpdater/VPUpdater.csproj @@ -93,6 +93,7 @@ + DownloadForm.cs From 264583dd46cb83a37bb31ca9636abd9841bce771 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 18:14:23 +0100 Subject: [PATCH 08/12] =?UTF-8?q?=E2=8F=AC=20Successfully=20download=20pre?= =?UTF-8?q?-release=20setups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FetchDownloadLink() must parse XML feed manually since the tag is actually - which is unsupported by SyndicationItem. Duplicate code here found in GetLatestVirtualParadisePost() but this seems to be unavoidable at this current moment. * Known issues: Right now, we get an exception when trying to fetch the version number of an alpha build --- VPUpdater/DownloadForm.cs | 20 +++-- VPUpdater/Updater.cs | 164 +++++++++++++++++++++++++++++--------- 2 files changed, 139 insertions(+), 45 deletions(-) diff --git a/VPUpdater/DownloadForm.cs b/VPUpdater/DownloadForm.cs index 0d22fe0..19e4e2e 100644 --- a/VPUpdater/DownloadForm.cs +++ b/VPUpdater/DownloadForm.cs @@ -99,10 +99,11 @@ private void ButtonCancel_Click(object sender, EventArgs e) /// /// Checks for a Virtual Paradise update. /// + /// The update channel to use. /// Returns if the there is an updated and the user accepted, otherwise. - private async Task CheckForUpdates() + private async Task CheckForUpdates(UpdateChannel channel) { - return await this.updater.FetchLatest(); + return await this.updater.FetchLatest(channel); } /// @@ -110,8 +111,10 @@ private async Task CheckForUpdates() /// /// The event sender. /// The event data. - private void DownloadForm_Load(object sender, EventArgs e) + private async void DownloadForm_Load(object sender, EventArgs e) { + await this.updater.LoadDefaultConfiguration(); + this.Show(); this.Run(); } @@ -138,10 +141,14 @@ private async void Run() return; } - this.labelDownloading.Text = Resources.UpdateCheck; + UpdateChannel channel = (int)this.updater.Config["stable_only"] == 1 + ? UpdateChannel.Stable + : UpdateChannel.PreRelease; + this.labelDownloading.Text = Resources.UpdateCheck; Version currentVersion = this.virtualParadise.Version; - Version latestVersion = await this.CheckForUpdates(); + Version latestVersion = await this.CheckForUpdates(channel); + if (currentVersion < latestVersion) { DialogResult result = MessageBox.Show( @@ -174,9 +181,10 @@ private async void Run() this.labelDownloading.Text = Resources.DownloadLinkFetch; Uri downloadUri; + try { - downloadUri = await this.updater.FetchDownloadLink(); + downloadUri = await this.updater.FetchDownloadLink(channel); } catch (Exception ex) { diff --git a/VPUpdater/Updater.cs b/VPUpdater/Updater.cs index 1be26f2..71cf9d4 100644 --- a/VPUpdater/Updater.cs +++ b/VPUpdater/Updater.cs @@ -13,6 +13,7 @@ namespace VPUpdater #region Using Directives using System; + using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -20,8 +21,10 @@ namespace VPUpdater using System.ServiceModel.Syndication; using System.Text.RegularExpressions; using System.Threading.Tasks; + using System.Windows.Forms; using System.Xml; using AngleSharp; + using AngleSharp.Common; using AngleSharp.Dom; using Properties; using SemVer = SemVer.Version; @@ -80,6 +83,15 @@ public Updater(VirtualParadise virtualParadise) #endregion + #region Properties + + /// + /// Gets the configuration for the updater. + /// + public UpdaterConfig Config { get; private set; } + + #endregion + #region Methods /// @@ -136,29 +148,17 @@ public async Task FetchLatest(UpdateChannel channel = UpdateChannel.Stab switch (channel) { case UpdateChannel.PreRelease: - string rssUri = new Uri(VirtualParadise.Uri, @"/edwin/feed").ToString(); - using (XmlReader rssReader = XmlReader.Create(rssUri)) - { - SyndicationFeed feed = SyndicationFeed.Load(rssReader); - Match match = null; - SyndicationItem item = - feed.Items.FirstOrDefault(i => Regex.Match(i.Title.Text, @"Virtual Paradise").Success && - (match = - Regex.Match( - i.Title.Text, SemVerRegex, - RegexOptions.IgnoreCase)) - .Success); + SyndicationItem item = this.GetLatestVirtualParadisePost(out Match match); - if (!(item is null || match is null)) - { - // Return the version - return new SemVer(match.Value); - } - - // Return the latest stable instead - return await this.FetchLatest(); + if (!(item is null || match is null)) + { + // Return the version + return new SemVer(match.Value); } + // Return the latest stable instead + return await this.FetchLatest(); + case UpdateChannel.Stable: default: using (WebClient client = new WebClient()) @@ -178,30 +178,81 @@ public async Task FetchLatest(UpdateChannel channel = UpdateChannel.Stab /// Fetches the Uri of the latest download from the Virtual Paradise download page. /// /// The update channel to use. - /// If is set to , the method will parse - /// this for a valid download URI. /// Returns a containing the download link. - public async Task FetchDownloadLink(UpdateChannel channel = UpdateChannel.Stable, - SyndicationItem item = null) + public async Task FetchDownloadLink(UpdateChannel channel = UpdateChannel.Stable) { - IConfiguration config = Configuration.Default.WithDefaultLoader(); - IBrowsingContext context = BrowsingContext.New(config); - Uri downloadPageUri = new Uri(VirtualParadise.Uri, @"Download"); + IConfiguration config = Configuration.Default.WithDefaultLoader(); + IBrowsingContext context = BrowsingContext.New(config); - using (IDocument document = await context.OpenAsync(downloadPageUri.ToString())) + switch (channel) { - const string selector = @".download a.btn"; - string systemArch = Helper.GetMachineArch().ToString(); - - IHtmlCollection cells = document.QuerySelectorAll(selector); - IElement a = - cells.FirstOrDefault(c => - Regex.Match(c.GetAttribute("href"), - $"windows_{Regex.Escape(systemArch)}") - .Success); + case UpdateChannel.PreRelease: + + // The RSS feed for the blog does not use as a tag, but instead + // uses - as such, we'll have to parse the XML manually + string rssUri = new Uri(VirtualParadise.Uri, @"edwin/feed").ToString(); + using (XmlReader rssReader = XmlReader.Create(rssUri)) + { + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.Load(rssReader); + + XmlNodeList items = xmlDocument.GetElementsByTagName("item"); + XmlNode useNode = null; + + foreach (XmlNode node in items) + { + string title = node["title"]?.InnerText ?? ""; + bool match = Regex.Match(title, @"Virtual Paradise").Success && + Regex.Match(title, SemVerRegex).Success; + + if (match) + { + useNode = node; + break; + } + } + + if (useNode is null) + { + return null; + } - string href = a?.GetAttribute("href") ?? ""; - return new Uri(href); + using (IDocument document = await context.OpenAsync(req => req.Content(useNode.InnerText))) + { + const string selector = @"a:last-child"; + string systemArch = Helper.GetMachineArch().ToString(); + + IHtmlCollection cells = document.QuerySelectorAll(selector); + IElement a = + cells.FirstOrDefault(c => + Regex.Match(c.GetAttribute("href"), + $"windows_{Regex.Escape(systemArch)}") + .Success); + + string href = a?.GetAttribute("href") ?? ""; + return new Uri(href); + } + } + + case UpdateChannel.Stable: + default: + Uri downloadPageUri = new Uri(VirtualParadise.Uri, @"Download"); + + using (IDocument document = await context.OpenAsync(downloadPageUri.ToString())) + { + const string selector = @".download a.btn"; + string systemArch = Helper.GetMachineArch().ToString(); + + IHtmlCollection cells = document.QuerySelectorAll(selector); + IElement a = + cells.FirstOrDefault(c => + Regex.Match(c.GetAttribute("href"), + $"windows_{Regex.Escape(systemArch)}") + .Success); + + string href = a?.GetAttribute("href") ?? ""; + return new Uri(href); + } } } @@ -224,6 +275,41 @@ await Task.Run(() => }); } + /// + /// Loads the configuration file, and sets any un-set default configuration values. + /// + public async Task LoadDefaultConfiguration() + { + this.Config = await UpdaterConfig.Load(); + this.Config["stable_only"] = this.Config["stable_only", 1]; + + await this.Config.Save(); + } + + /// + /// Gets the latest Virtual Paradise post from Edwin's blog. + /// + /// Returns an instance of . + private SyndicationItem GetLatestVirtualParadisePost(out Match match) + { + string rssUri = new Uri(VirtualParadise.Uri, @"edwin/feed").ToString(); + using (XmlReader rssReader = XmlReader.Create(rssUri)) + { + Match localMatch = null; + SyndicationFeed feed = SyndicationFeed.Load(rssReader); + SyndicationItem item = + feed.Items.FirstOrDefault(i => Regex.Match(i.Title.Text, @"Virtual Paradise").Success && + (localMatch = + Regex.Match( + i.Title.Text, SemVerRegex, + RegexOptions.IgnoreCase)) + .Success); + + match = localMatch; + return item; + } + } + #endregion } } From 8565f054c213fba34e13dde89683989c76483c25 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 18:22:59 +0100 Subject: [PATCH 09/12] =?UTF-8?q?=F0=9F=93=9A=20Add=20new=20sections=20to?= =?UTF-8?q?=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "How to enable pre-release builds" and "Known issues" --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 871b964..30ea80a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,18 @@ I've rewritten the tool from the ground up in C# to correct all of these issues. Now when you launch "Virtual Paradise", it will launch the updater first. If there are no updates, the tool with simply launch Virtual Paradise. If there is an update, it will download the latest setup and you can go from there. +## How to enable pre-release builds +Edit (or create) file `VPUpdater.cfg` in the Virtual Paradise directory with the line: +```properties +stable_only=0 +``` +The next time the updater runs, it will check for pre-release builds. + +## Known issues +* If pre-release builds are enabled, it will always say there is an update even if you have it. Pre-release builds install into `%PROGRAMFILES%\Virtual Paradise (pre-release)`, where VPUpdater probably *isn't*. Possible fix: Have the app scan this directory too for the Virtual Paradise version number. + +* An exception is thrown trying to fetch the version number of a pre-release install of Virtual Paradise. Possible fix (?): Truncate string if it contains `"alpha"` or `"beta"` perhaps? + ## Building prerequisites |Prerequisite|Version| |- |- | From 4e0659c129d03796317416a05bb31ae29db88d16 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 19:06:29 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=94=A8=20Build=20DownloadForm=20usi?= =?UTF-8?q?ng=20async=20builder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ctor is made private, and simply stores values instead of heavy file reading --- VPUpdater/DownloadForm.cs | 48 +++++++++++++++++++++++++----------- VPUpdater/Program.cs | 9 +++++-- VPUpdater/Updater.cs | 20 --------------- VPUpdater/UpdaterConfig.cs | 10 ++++++++ VPUpdater/VirtualParadise.cs | 13 ++++++++++ 5 files changed, 63 insertions(+), 37 deletions(-) diff --git a/VPUpdater/DownloadForm.cs b/VPUpdater/DownloadForm.cs index 19e4e2e..d150bfe 100644 --- a/VPUpdater/DownloadForm.cs +++ b/VPUpdater/DownloadForm.cs @@ -31,11 +31,10 @@ public partial class DownloadForm : Form { #region Fields - private string setupTempFile = ""; private readonly string[] commandLineArgs; - private readonly WebClient client = new WebClient(); private readonly Updater updater; private readonly VirtualParadise virtualParadise; + private readonly UpdateChannel updateChannel; #endregion @@ -44,20 +43,45 @@ public partial class DownloadForm : Form /// /// Initializes a new instance of the class. /// - public DownloadForm(string[] args) + /// Command-line arguments to pass to Virtual Paradise. + /// The instance of to use. + /// The update channel to use. + private DownloadForm(string[] args, VirtualParadise virtualParadise, UpdateChannel channel) { this.InitializeComponent(); - // Store CLI args as DI to pass to VP this.commandLineArgs = args; - this.virtualParadise = VirtualParadise.GetCurrent(); - this.updater = new Updater(this.virtualParadise); + this.virtualParadise = virtualParadise; + this.updateChannel = channel; + this.updater = new Updater(virtualParadise); } #endregion #region Methods + /// + /// Builds a . + /// + /// Command-line arguments to pass to Virtual Paradise. + /// Returns a new instance of . + public static async Task Build(string[] args) + { + UpdaterConfig config = await UpdaterConfig.Load(); + + await config.LoadDefaults(); + + UpdateChannel channel = (int)config["stable_only", 1] == 1 + ? UpdateChannel.Stable + : UpdateChannel.PreRelease; + + VirtualParadise virtualParadise = channel == UpdateChannel.PreRelease + ? VirtualParadise.GetPreRelease() + : VirtualParadise.GetCurrent(); + + return new DownloadForm(args, virtualParadise, channel); + } + /// /// Called when is clicked. /// @@ -111,10 +135,8 @@ private async Task CheckForUpdates(UpdateChannel channel) /// /// The event sender. /// The event data. - private async void DownloadForm_Load(object sender, EventArgs e) + private void DownloadForm_Load(object sender, EventArgs e) { - await this.updater.LoadDefaultConfiguration(); - this.Show(); this.Run(); } @@ -141,13 +163,9 @@ private async void Run() return; } - UpdateChannel channel = (int)this.updater.Config["stable_only"] == 1 - ? UpdateChannel.Stable - : UpdateChannel.PreRelease; - this.labelDownloading.Text = Resources.UpdateCheck; Version currentVersion = this.virtualParadise.Version; - Version latestVersion = await this.CheckForUpdates(channel); + Version latestVersion = await this.CheckForUpdates(this.updateChannel); if (currentVersion < latestVersion) { @@ -184,7 +202,7 @@ private async void Run() try { - downloadUri = await this.updater.FetchDownloadLink(channel); + downloadUri = await this.updater.FetchDownloadLink(this.updateChannel); } catch (Exception ex) { diff --git a/VPUpdater/Program.cs b/VPUpdater/Program.cs index 4a59447..0f7ef67 100644 --- a/VPUpdater/Program.cs +++ b/VPUpdater/Program.cs @@ -14,6 +14,7 @@ namespace VPUpdater using System; using System.Linq; + using System.Threading.Tasks; using System.Windows.Forms; #endregion @@ -27,13 +28,17 @@ internal static class Program /// Application entry point. /// [STAThread] - private static void Main(string[] args) + private static async Task Main(string[] args) { args = args.Skip(1).ToArray(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new DownloadForm(args)); + + using (DownloadForm form = await DownloadForm.Build(args)) + { + Application.Run(form); + } } } } diff --git a/VPUpdater/Updater.cs b/VPUpdater/Updater.cs index 71cf9d4..b8a9364 100644 --- a/VPUpdater/Updater.cs +++ b/VPUpdater/Updater.cs @@ -83,15 +83,6 @@ public Updater(VirtualParadise virtualParadise) #endregion - #region Properties - - /// - /// Gets the configuration for the updater. - /// - public UpdaterConfig Config { get; private set; } - - #endregion - #region Methods /// @@ -275,17 +266,6 @@ await Task.Run(() => }); } - /// - /// Loads the configuration file, and sets any un-set default configuration values. - /// - public async Task LoadDefaultConfiguration() - { - this.Config = await UpdaterConfig.Load(); - this.Config["stable_only"] = this.Config["stable_only", 1]; - - await this.Config.Save(); - } - /// /// Gets the latest Virtual Paradise post from Edwin's blog. /// diff --git a/VPUpdater/UpdaterConfig.cs b/VPUpdater/UpdaterConfig.cs index e8f9429..068431b 100644 --- a/VPUpdater/UpdaterConfig.cs +++ b/VPUpdater/UpdaterConfig.cs @@ -124,6 +124,16 @@ public static async Task Load() } } + /// + /// Loads the default configuration. + /// + public async Task LoadDefaults() + { + this["stable_only"] = this["stable_only", 1]; + + await this.Save(); + } + /// /// Saves the configuration to file. /// diff --git a/VPUpdater/VirtualParadise.cs b/VPUpdater/VirtualParadise.cs index ffbd0ca..f1492bb 100644 --- a/VPUpdater/VirtualParadise.cs +++ b/VPUpdater/VirtualParadise.cs @@ -110,6 +110,19 @@ public static VirtualParadise GetCurrent(string path) return File.Exists(filename) ? new VirtualParadise(new FileInfo(filename)) : null; } + /// + /// Gets the pre-release version of Virtual Paradise. + /// + /// Returns a new instance of , or on failure. + public static VirtualParadise GetPreRelease() + { + string path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + + Path.DirectorySeparatorChar + + @"Virtual Paradise (pre-release)"; + + return GetCurrent(path); + } + /// /// Translates a into a semantic version compliant . /// From b091006b28109eb0bd7f3d0eaa89cd3bd834fdab Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 19:31:26 +0100 Subject: [PATCH 11/12] =?UTF-8?q?=E2=9C=A8=20Successfully=20integrate=20fe?= =?UTF-8?q?ature=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * VPUpdater will scan Pre-Release directory for Virtual Paradise * Construct Semantic Version if System.Version.Parse fails * For some reason, cross-Thread UI-access exceptions were being thrown, so I've implemented an extension method InvokeIfRequired() to call Invoke() if InvokeRequired is true --- VPUpdater/ControlExtensions.cs | 41 ++++++++++ VPUpdater/DownloadForm.cs | 90 ++++++++++++++++------ VPUpdater/Properties/Resources.Designer.cs | 11 +++ VPUpdater/Properties/Resources.resx | 5 ++ VPUpdater/VPUpdater.csproj | 1 + VPUpdater/VirtualParadise.cs | 32 ++++++-- 6 files changed, 148 insertions(+), 32 deletions(-) create mode 100644 VPUpdater/ControlExtensions.cs diff --git a/VPUpdater/ControlExtensions.cs b/VPUpdater/ControlExtensions.cs new file mode 100644 index 0000000..33bfe4d --- /dev/null +++ b/VPUpdater/ControlExtensions.cs @@ -0,0 +1,41 @@ +#region Copyright + +// ----------------------------------------------------------------------- +// +// (C) 2019 Oliver Booth. All rights reserved. +// +// ----------------------------------------------------------------------- + +#endregion + +namespace VPUpdater +{ + #region Using Directives + + using System.Windows.Forms; + + #endregion + + /// + /// Extension methods for . + /// + public static class ControlExtensions + { + /// + /// Thread-safe method invocation. Calls if returns . + /// + /// The control from which to invoke. + /// The action to invoke. + public static void InvokeIfRequired(this Control control, MethodInvoker action) + { + if (control?.InvokeRequired ?? false) + { + control.Invoke(action); + } + else + { + action(); + } + } + } +} diff --git a/VPUpdater/DownloadForm.cs b/VPUpdater/DownloadForm.cs index d150bfe..5898164 100644 --- a/VPUpdater/DownloadForm.cs +++ b/VPUpdater/DownloadForm.cs @@ -13,9 +13,6 @@ namespace VPUpdater #region Using Directives using System; - using System.ComponentModel; - using System.Diagnostics; - using System.IO; using System.Net; using System.Threading.Tasks; using System.Windows.Forms; @@ -75,11 +72,31 @@ public static async Task Build(string[] args) ? UpdateChannel.Stable : UpdateChannel.PreRelease; - VirtualParadise virtualParadise = channel == UpdateChannel.PreRelease - ? VirtualParadise.GetPreRelease() - : VirtualParadise.GetCurrent(); + try + { + VirtualParadise virtualParadise = VirtualParadise.GetCurrent(); + if (channel == UpdateChannel.PreRelease) + { + VirtualParadise preVirtualParadise = VirtualParadise.GetPreRelease(); + if (!(preVirtualParadise is null)) + { + virtualParadise = preVirtualParadise; + } + } - return new DownloadForm(args, virtualParadise, channel); + return new DownloadForm(args, virtualParadise, channel); + } + catch (Exception ex) + { + MessageBox.Show(String.Format(Resources.VpObjectBuildError, ex.Message), + Resources.Error, + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + // There's nothing we can do from here + Environment.Exit(0); + return null; + } } /// @@ -135,24 +152,34 @@ private async Task CheckForUpdates(UpdateChannel channel) /// /// The event sender. /// The event data. - private void DownloadForm_Load(object sender, EventArgs e) + private async void DownloadForm_Load(object sender, EventArgs e) { this.Show(); - this.Run(); + this.InvokeIfRequired(async () => await this.Run()); } /// /// Performs the update routine. /// - private async void Run() + private async Task Run() { - this.labelDownloading.Text = String.Format(Resources.VpExeCheck, VirtualParadise.ExeFilename); - this.progressBar.Style = ProgressBarStyle.Marquee; + this.InvokeIfRequired( + () => + { + this.labelDownloading.Text = + String.Format(Resources.VpExeCheck, VirtualParadise.ExeFilename); + + this.progressBar.Style = ProgressBarStyle.Marquee; + }); if (this.virtualParadise == null) { - this.progressBar.Style = ProgressBarStyle.Continuous; - this.progressBar.Value = 0; + this.InvokeIfRequired( + () => + { + this.progressBar.Style = ProgressBarStyle.Continuous; + this.progressBar.Value = 0; + }); MessageBox.Show(String.Format(Resources.VpExeNotFound, VirtualParadise.ExeFilename), Resources.Error, @@ -163,7 +190,7 @@ private async void Run() return; } - this.labelDownloading.Text = Resources.UpdateCheck; + this.InvokeIfRequired(() => { this.labelDownloading.Text = Resources.UpdateCheck; }); Version currentVersion = this.virtualParadise.Version; Version latestVersion = await this.CheckForUpdates(this.updateChannel); @@ -186,10 +213,14 @@ private async void Run() else { // Everything is up to date! - this.labelDownloading.Text = Resources.UpToDate; - this.progressBar.Style = ProgressBarStyle.Continuous; - this.progressBar.Value = this.progressBar.Maximum; - this.buttonCancel.Text = Resources.Close; + this.InvokeIfRequired( + () => + { + this.labelDownloading.Text = Resources.UpToDate; + this.progressBar.Style = ProgressBarStyle.Continuous; + this.progressBar.Value = this.progressBar.Maximum; + this.buttonCancel.Text = Resources.Close; + }); this.virtualParadise.Launch(this.commandLineArgs); @@ -197,7 +228,7 @@ private async void Run() return; } - this.labelDownloading.Text = Resources.DownloadLinkFetch; + this.InvokeIfRequired(() => { this.labelDownloading.Text = Resources.DownloadLinkFetch; }); Uri downloadUri; try @@ -255,8 +286,12 @@ private async void Run() return; } - this.progressBar.Style = ProgressBarStyle.Marquee; - this.labelDownloading.Text = Resources.WaitingForSetup; + this.InvokeIfRequired( + () => + { + this.progressBar.Style = ProgressBarStyle.Marquee; + this.labelDownloading.Text = Resources.WaitingForSetup; + }); try { @@ -296,9 +331,14 @@ private async void Run() private void WebClientProgressChanged(object sender, DownloadProgressChangedEventArgs e) { // Update progress for user - this.progressBar.Style = ProgressBarStyle.Continuous; - this.progressBar.Value = e.ProgressPercentage; - this.labelDownloading.Text = String.Format(Resources.DownloadingUpdate, e.ProgressPercentage); + this.InvokeIfRequired( + () => + { + this.progressBar.Style = ProgressBarStyle.Continuous; + this.progressBar.Value = e.ProgressPercentage; + this.labelDownloading.Text = + String.Format(Resources.DownloadingUpdate, e.ProgressPercentage); + }); } #endregion diff --git a/VPUpdater/Properties/Resources.Designer.cs b/VPUpdater/Properties/Resources.Designer.cs index 0db749e..3e272b4 100644 --- a/VPUpdater/Properties/Resources.Designer.cs +++ b/VPUpdater/Properties/Resources.Designer.cs @@ -270,6 +270,17 @@ internal static string VpExeNotFound { } } + /// + /// Looks up a localized string similar to Could not create VirtualParadise object: + /// + ///{0}. + /// + internal static string VpObjectBuildError { + get { + return ResourceManager.GetString("VpObjectBuildError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Waiting for setup to complete.... /// diff --git a/VPUpdater/Properties/Resources.resx b/VPUpdater/Properties/Resources.resx index 65d3c62..67e86e7 100644 --- a/VPUpdater/Properties/Resources.resx +++ b/VPUpdater/Properties/Resources.resx @@ -199,4 +199,9 @@ Current version: {0} The provided download URI is not from {0}. + + Could not create VirtualParadise object: + +{0} + \ No newline at end of file diff --git a/VPUpdater/VPUpdater.csproj b/VPUpdater/VPUpdater.csproj index 29895ae..7587930 100644 --- a/VPUpdater/VPUpdater.csproj +++ b/VPUpdater/VPUpdater.csproj @@ -80,6 +80,7 @@ + diff --git a/VPUpdater/VirtualParadise.cs b/VPUpdater/VirtualParadise.cs index f1492bb..a91c7bc 100644 --- a/VPUpdater/VirtualParadise.cs +++ b/VPUpdater/VirtualParadise.cs @@ -52,15 +52,33 @@ public class VirtualParadise /// The file information for Virtual Paradise. private VirtualParadise(FileInfo current) { - // Virtual Paradise does not use the Semantic Version standard, - // but we can use System.Version as a middle-man, and build a SemVer-complaint - // version from its properties. - // Edwin pls. https://semver.org/ - you can thank me later. string fileVersion = FileVersionInfo.GetVersionInfo(current.FullName).FileVersion; - SysVer version = SysVer.Parse(fileVersion); - this.FileInfo = current; - this.Version = GetSemVerFromSystemVersion(version); + + try + { + // Virtual Paradise does not use the Semantic Version standard, + // but we can use System.Version as a middle-man, and build a SemVer-complaint + // version from its properties. + // Edwin pls. https://semver.org/ - you can thank me later. + SysVer version = SysVer.Parse(fileVersion); + this.Version = GetSemVerFromSystemVersion(version); + } + catch + { + try + { + // pre-release builds *seem* to use Semantic Version strings + // and so if System.Version failed to parse, we can just build + // a SemVer object immediately + this.Version = new SemVer(fileVersion); + } + catch + { + // if THAT fails, then I'll need to come back to this... + throw new Exception(@"Could not parse version string."); + } + } } #endregion From bbcc7b4c626a46f5198d3f03bdaa16bdb3b8de1a Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Fri, 28 Jun 2019 19:32:12 +0100 Subject: [PATCH 12/12] =?UTF-8?q?=F0=9F=97=91=20Remove=20"Known=20issues"?= =?UTF-8?q?=20from=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These issues were resolved and no longer need to be written about --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 30ea80a..1d4f746 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,6 @@ stable_only=0 ``` The next time the updater runs, it will check for pre-release builds. -## Known issues -* If pre-release builds are enabled, it will always say there is an update even if you have it. Pre-release builds install into `%PROGRAMFILES%\Virtual Paradise (pre-release)`, where VPUpdater probably *isn't*. Possible fix: Have the app scan this directory too for the Virtual Paradise version number. - -* An exception is thrown trying to fetch the version number of a pre-release install of Virtual Paradise. Possible fix (?): Truncate string if it contains `"alpha"` or `"beta"` perhaps? - ## Building prerequisites |Prerequisite|Version| |- |- |