beginnings of v4.8.0.0

- Minor refactoring
- Added an HTTP content cache
- Replaced Onova with custom update functionality (work in progress)
This commit is contained in:
pointfeev 2023-03-30 02:19:45 -04:00
parent 52e82e2397
commit 74c5588d64
9 changed files with 219 additions and 249 deletions

View file

@ -4,7 +4,7 @@
<TargetFramework>net7.0-windows</TargetFramework>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Resources\ini.ico</ApplicationIcon>
<Version>4.7.1.3</Version>
<Version>4.8.0.0</Version>
<Copyright>2021, pointfeev (https://github.com/pointfeev)</Copyright>
<Company>CreamInstaller</Company>
<Product>Automatic DLC Unlocker Installer &amp; Configuration Generator</Product>
@ -147,7 +147,6 @@
<PackageReference Include="Gameloop.Vdf" Version="0.6.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Onova" Version="2.6.6" />
<PackageReference Include="System.ServiceModel.Primitives" Version="4.10.0" />
</ItemGroup>
<ItemGroup>

View file

@ -1,240 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Windows.Forms;
using CreamInstaller.Components;
using CreamInstaller.Utility;
using HtmlAgilityPack;
using Onova;
using Onova.Models;
using Onova.Services;
namespace CreamInstaller.Forms;
internal sealed partial class MainForm : CustomForm
{
private CancellationTokenSource cancellationTokenSource;
private Version latestVersion;
private UpdateManager updateManager;
private IReadOnlyList<Version> versions;
internal MainForm()
{
InitializeComponent();
Text = Program.ApplicationNameShort;
}
private void StartProgram()
{
if (cancellationTokenSource is not null)
{
cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
}
#pragma warning disable CA2000 // Dispose objects before losing scope
SelectForm form = new();
#pragma warning restore CA2000 // Dispose objects before losing scope
form.InheritLocation(this);
form.FormClosing += (_, _) => Close();
form.Show();
Hide();
#if DEBUG
DebugForm.Current.Attach(form);
#endif
}
private async void OnLoad()
{
progressBar.Visible = false;
ignoreButton.Visible = true;
updateButton.Text = "Update";
updateButton.Click -= OnUpdateCancel;
progressLabel.Text = "Checking for updates . . .";
changelogTreeView.Visible = false;
changelogTreeView.Location = progressLabel.Location with { Y = progressLabel.Location.Y + progressLabel.Size.Height + 13 };
Refresh();
#if DEBUG
DebugForm.Current.Attach(this);
#endif
GithubPackageResolver resolver = new(Program.RepositoryOwner, Program.RepositoryName, Program.RepositoryPackage);
ZipPackageExtractor extractor = new();
updateManager = new(AssemblyMetadata.FromAssembly(Program.EntryAssembly, Program.CurrentProcessFilePath), resolver, extractor);
if (latestVersion is null)
{
cancellationTokenSource = new();
try
{
CheckForUpdatesResult checkForUpdatesResult = await updateManager.CheckForUpdatesAsync(cancellationTokenSource.Token);
#if !DEBUG
if (checkForUpdatesResult.CanUpdate)
{
#endif
latestVersion = checkForUpdatesResult.LastVersion;
versions = checkForUpdatesResult.Versions;
#if !DEBUG
}
#endif
}
#if DEBUG
catch (TaskCanceledException) { }
catch (Exception e)
{
DebugForm.Current.Log($"Exception while checking for updates: {e.GetType()} ({e.Message})", LogTextBox.Warning);
}
#else
catch
{
// ignored
}
#endif
finally
{
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
}
}
if (latestVersion is null)
{
updateManager.Dispose();
updateManager = null;
StartProgram();
}
else
{
progressLabel.Text = $"An update is available: v{latestVersion}";
ignoreButton.Enabled = true;
updateButton.Enabled = true;
updateButton.Click += OnUpdate;
changelogTreeView.Visible = true;
Version currentVersion = new(Program.Version);
#if DEBUG
foreach (Version version in versions.Where(v => (v > currentVersion || v == latestVersion) && !changelogTreeView.Nodes.ContainsKey(v.ToString())))
#else
foreach (Version version in versions.Where(v => v > currentVersion && !changelogTreeView.Nodes.ContainsKey(v.ToString())))
#endif
{
TreeNode root = new($"v{version}") { Name = version.ToString() };
changelogTreeView.Nodes.Add(root);
if (changelogTreeView.Nodes.Count > 0)
changelogTreeView.Nodes[0].EnsureVisible();
_ = Task.Run(async () =>
{
HtmlNodeCollection nodes = await HttpClientManager.GetDocumentNodes(
$"https://github.com/{Program.RepositoryOwner}/{Program.RepositoryName}/releases/tag/v{version}",
"//div[@data-test-selector='body-content']/ul/li");
if (nodes is null)
changelogTreeView.Nodes.Remove(root);
else
foreach (HtmlNode node in nodes)
changelogTreeView.Invoke(delegate
{
TreeNode change = new() { Text = HttpUtility.HtmlDecode(node.InnerText) ?? string.Empty };
root.Nodes.Add(change);
root.Expand();
if (changelogTreeView.Nodes.Count > 0)
changelogTreeView.Nodes[0].EnsureVisible();
});
});
}
}
}
private void OnLoad(object sender, EventArgs _)
{
retry:
try
{
string fileName = Path.GetFileName(Program.CurrentProcessFilePath);
if (fileName != Program.ApplicationExecutable)
{
using DialogForm form = new(this);
if (form.Show(SystemIcons.Warning,
"WARNING: " + Program.ApplicationExecutable + " was renamed!" + "\n\nThis will cause undesirable behavior when updating the program!",
"Ignore", "Abort") == DialogResult.Cancel)
{
Application.Exit();
return;
}
}
OnLoad();
}
catch (Exception e)
{
if (e.HandleException(this))
goto retry;
Close();
}
}
private void OnIgnore(object sender, EventArgs e) => StartProgram();
private async void OnUpdate(object sender, EventArgs e)
{
progressBar.Visible = true;
ignoreButton.Visible = false;
updateButton.Text = "Cancel";
updateButton.Click -= OnUpdate;
updateButton.Click += OnUpdateCancel;
changelogTreeView.Location = progressBar.Location with { Y = progressBar.Location.Y + progressBar.Size.Height + 6 };
Refresh();
Progress<double> progress = new();
progress.ProgressChanged += delegate(object _, double _progress)
{
progressLabel.Text = $"Updating . . . {(int)_progress}%";
progressBar.Value = (int)_progress;
};
progressLabel.Text = "Updating . . . ";
cancellationTokenSource = new();
try
{
await updateManager.PrepareUpdateAsync(latestVersion, progress, cancellationTokenSource.Token);
}
#if DEBUG
catch (TaskCanceledException) { }
catch (Exception ex)
{
DebugForm.Current.Log($"Exception while preparing update: {ex.GetType()} ({ex.Message})", LogTextBox.Warning);
}
#else
catch
{
// ignored
}
#endif
finally
{
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
}
if (updateManager is not null && updateManager.IsUpdatePrepared(latestVersion))
{
updateManager.LaunchUpdater(latestVersion);
Application.Exit();
return;
}
OnLoad();
}
private void OnUpdateCancel(object sender, EventArgs e)
{
cancellationTokenSource?.Cancel();
updateManager?.Dispose();
updateManager = null;
}
protected override void Dispose(bool disposing)
{
if (disposing)
components?.Dispose();
base.Dispose(disposing);
cancellationTokenSource?.Dispose();
updateManager?.Dispose();
}
}

View file

@ -4,7 +4,7 @@ using CreamInstaller.Components;
namespace CreamInstaller.Forms
{
partial class MainForm
partial class UpdateForm
{
private IContainer components = null;
@ -78,7 +78,7 @@ namespace CreamInstaller.Forms
this.changelogTreeView.Sorted = true;
this.changelogTreeView.TabIndex = 5;
//
// MainForm
// UpdateForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
@ -94,9 +94,9 @@ namespace CreamInstaller.Forms
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "MainForm";
this.Name = "UpdateForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "MainForm";
this.Text = "UpdateForm";
this.Load += new System.EventHandler(this.OnLoad);
this.ResumeLayout(false);

View file

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using CreamInstaller.Components;
using CreamInstaller.Utility;
using Newtonsoft.Json;
namespace CreamInstaller.Forms;
internal sealed partial class UpdateForm : CustomForm
{
private Release latestRelease;
internal UpdateForm()
{
InitializeComponent();
Text = Program.ApplicationNameShort;
}
private void StartProgram()
{
SelectForm form = new();
form.InheritLocation(this);
form.FormClosing += (_, _) => Close();
form.Show();
Hide();
#if DEBUG
DebugForm.Current.Attach(form);
#endif
}
private async void OnLoad()
{
progressBar.Visible = false;
ignoreButton.Visible = true;
updateButton.Text = "Update";
updateButton.Click -= OnUpdateCancel;
progressLabel.Text = "Checking for updates . . .";
changelogTreeView.Visible = false;
changelogTreeView.Location = progressLabel.Location with { Y = progressLabel.Location.Y + progressLabel.Size.Height + 13 };
Refresh();
#if !DEBUG
Version currentVersion = new(Program.Version);
#endif
List<Release> releases = null;
string response = await HttpClientManager.EnsureGet($"https://api.github.com/repos/{Program.RepositoryOwner}/{Program.RepositoryName}/releases");
if (response is not null)
releases = JsonConvert.DeserializeObject<List<Release>>(response)
?.Where(release => !release.Draft && !release.Prerelease && release.Assets.Count > 0).ToList();
latestRelease = releases?.FirstOrDefault();
#if DEBUG
if (latestRelease?.Version is not { } latestVersion)
#else
if (latestRelease?.Version is not { } latestVersion || latestVersion <= currentVersion)
#endif
StartProgram();
else
{
progressLabel.Text = $"An update is available: v{latestVersion}";
ignoreButton.Enabled = true;
updateButton.Enabled = true;
updateButton.Click += OnUpdate;
changelogTreeView.Visible = true;
for (int r = releases!.Count - 1; r >= 0; r--)
{
Release release = releases[r];
#if !DEBUG
if (release.Version <= currentVersion)
continue;
#endif
TreeNode root = new(release.Name) { Name = release.Name };
changelogTreeView.Nodes.Add(root);
if (changelogTreeView.Nodes.Count > 0)
changelogTreeView.Nodes[0].EnsureVisible();
for (int i = release.Changes.Length - 1; i >= 0; i--)
changelogTreeView.Invoke(delegate
{
string change = release.Changes[i];
TreeNode changeNode = new() { Text = change };
root.Nodes.Add(changeNode);
root.Expand();
if (changelogTreeView.Nodes.Count > 0)
changelogTreeView.Nodes[0].EnsureVisible();
});
}
}
}
private void OnLoad(object sender, EventArgs _)
{
retry:
try
{
string fileName = Path.GetFileName(Program.CurrentProcessFilePath);
if (fileName != Program.ApplicationExecutable)
{
using DialogForm form = new(this);
if (form.Show(SystemIcons.Warning,
"WARNING: " + Program.ApplicationExecutable + " was renamed!" + "\n\nThis will cause undesirable behavior when updating the program!",
"Ignore", "Abort") == DialogResult.Cancel)
{
Application.Exit();
return;
}
}
OnLoad();
}
catch (Exception e)
{
if (e.HandleException(this))
goto retry;
Close();
}
}
private void OnIgnore(object sender, EventArgs e) => StartProgram();
private void OnUpdate(object sender, EventArgs e)
{
progressBar.Visible = true;
ignoreButton.Visible = false;
updateButton.Text = "Cancel";
updateButton.Click -= OnUpdate;
updateButton.Click += OnUpdateCancel;
changelogTreeView.Location = progressBar.Location with { Y = progressBar.Location.Y + progressBar.Size.Height + 6 };
Refresh();
Progress<double> progress = new();
progress.ProgressChanged += delegate(object _, double _progress)
{
progressLabel.Text = $"Updating . . . {(int)_progress}%";
progressBar.Value = (int)_progress;
};
progressLabel.Text = "Updating . . . ";
// do update
OnLoad();
}
private void OnUpdateCancel(object sender, EventArgs e)
{
// cancel update
}
protected override void Dispose(bool disposing)
{
if (disposing)
components?.Dispose();
base.Dispose(disposing);
}
}

View file

@ -40,7 +40,7 @@ internal static class SteamStore
string response = await HttpClientManager.EnsureGet($"https://store.steampowered.com/api/appdetails?appids={appId}");
if (response is not null)
{
IDictionary<string, JToken> apps = (IDictionary<string, JToken>)JsonConvert.DeserializeObject(response);
Dictionary<string, JToken> apps = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(response);
if (apps is not null)
foreach (KeyValuePair<string, JToken> app in apps)
try

View file

@ -86,7 +86,7 @@ internal static class Program
try
{
HttpClientManager.Setup();
using MainForm form = new();
using UpdateForm form = new();
#if DEBUG
DebugForm.Current.Attach(form);
#endif

52
CreamInstaller/Release.cs Normal file
View file

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CreamInstaller;
public class Release
{
private string[] changes;
private Version version;
[JsonProperty("tag_name", NullValueHandling = NullValueHandling.Ignore)]
public string TagName { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty("draft", NullValueHandling = NullValueHandling.Ignore)]
public bool Draft { get; set; }
[JsonProperty("prerelease", NullValueHandling = NullValueHandling.Ignore)]
public bool Prerelease { get; set; }
[JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)]
public List<Asset> Assets { get; } = new();
[JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)]
public string Body { get; set; }
public Version Version => version ??= new(TagName[1..]);
public string[] Changes => changes ??= Body.Replace("- ", "").Split("\r\n");
}
public class Asset
{
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)]
public string ContentType { get; set; }
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)]
public string State { get; set; }
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
public int Size { get; set; }
[JsonProperty("browser_download_url", NullValueHandling = NullValueHandling.Ignore)]
public string BrowserDownloadUrl { get; set; }
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Net;
using System.Net.Http;
@ -14,6 +15,8 @@ internal static class HttpClientManager
{
internal static HttpClient HttpClient;
private static readonly Dictionary<string, string> HttpContentCache = new();
internal static void Setup()
{
HttpClient = new();
@ -26,8 +29,12 @@ internal static class HttpClientManager
{
using HttpRequestMessage request = new(HttpMethod.Get, url);
using HttpResponseMessage response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode is HttpStatusCode.NotModified && HttpContentCache.TryGetValue(url, out string content))
return content;
_ = response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
content = await response.Content.ReadAsStringAsync();
HttpContentCache[url] = content;
return content;
}
catch (HttpRequestException e)
{