Skip to content

Commit

Permalink
feat: Add update checking + downloading
Browse files Browse the repository at this point in the history
  • Loading branch information
ManuelKlaer committed Jun 5, 2023
1 parent 1876fb2 commit 2c10e5d
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 0 deletions.
1 change: 1 addition & 0 deletions Minesweeper.Package/Minesweeper.Package.wapproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>neutral</AppxBundlePlatforms>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
Expand Down
1 change: 1 addition & 0 deletions Minesweeper.Package/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@

<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient"/>
</Capabilities>
</Package>
2 changes: 2 additions & 0 deletions Minesweeper/Controllers/LanguageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public class LanguageResource
public string AppConfirmNewGameText => _resourceManager.GetString("AppConfirmNewGameText")!;
public string AppConfirmExitTitle => _resourceManager.GetString("AppConfirmExitTitle")!;
public string AppConfirmExitText => _resourceManager.GetString("AppConfirmExitText")!;
public string AppUpdateTitle => _resourceManager.GetString("AppUpdateTitle")!;
public string AppUpdateText => _resourceManager.GetString("AppUpdateText")!;
public string EmojiBomb => _resourceManager.GetString("EmojiBomb")!;
public string EmojiFlag => _resourceManager.GetString("EmojiFlag")!;
public string EmojiQuestion => _resourceManager.GetString("EmojiQuestion")!;
Expand Down
168 changes: 168 additions & 0 deletions Minesweeper/Controllers/UpdateController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Net.Http.Headers;
using Minesweeper.Models;
using Minesweeper.Utils.Helpers;
using Newtonsoft.Json;

namespace Minesweeper.Controllers;

public static class UpdateController
{
/// <summary>
/// Check if an update is available using the GitHub api.
/// </summary>
/// <returns>All necessary parameters of the latest update.</returns>
public static UpdateModel CheckForUpdate()
{
const string apiBase = "https://api.github.com";
const string apiPath = "repos/ManuelKlaer/windows-forms-minesweeper/releases?per_page=1";

// Initialize a new HttpClient
using HttpClient client = new();
client.BaseAddress = new Uri(apiBase);
client.DefaultRequestHeaders.Add("User-Agent", "MinesweeperUpdateCheck");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// Make an api request
HttpResponseMessage response = client.GetAsync(apiPath).Result;
response.EnsureSuccessStatusCode();

// Deserialize the received json object
dynamic jsonData = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result) ??
throw new InvalidOperationException("Couldn't deserialize json data from api.");

// Load values from json
int id = jsonData[0].id;
string versionCode = jsonData[0].tag_name;
string name = jsonData[0].name;
string body = jsonData[0].body;
string author = jsonData[0].author.login;
bool isPreRelease = jsonData[0].prerelease;
bool isDraft = jsonData[0].draft;
string url = jsonData[0].html_url;

// Find download packages
string? downloadPortable = null;
string? downloadInstaller = null;

foreach (dynamic asset in jsonData[0].assets)
{
string assetName = asset.name;
string assetUrl = asset.browser_download_url;

if (assetName.EndsWith(".zip")) downloadPortable = assetUrl;
else if (assetName.EndsWith(".appxbundle")) downloadInstaller = assetUrl;
}

// Create new UpdateModel and return it
return new UpdateModel
{
Id = id,
Version = UtilsClass.GetVersion(versionCode),
Name = name,
Body = body,
Author = author,
IsPreRelease = isPreRelease,
IsDraft = isDraft,
Url = url,
DownloadPortableUrl = downloadPortable,
DownloadInstallerUrl = downloadInstaller
};
}

/// <summary>
/// Install an update.
/// </summary>
/// <param name="update">The update to install.</param>
public static void InstallUpdate(UpdateModel update)
{
string downloadLocation = Path.Join(ApplicationInfo.StorageLocation, "tmp_update");
string downloadFile = Path.Join(downloadLocation, ApplicationInfo.IsAppxPackage ? "Minesweeper.appxbundle" : "Minesweeper.zip");

// Delete any old directory which may exists
try
{
if (Directory.Exists(downloadLocation)) Directory.Delete(downloadLocation, true);
}
catch (UnauthorizedAccessException) { }
catch (IOException) { }

// Create a temporary directory to store the downloaded files
if (!Directory.Exists(downloadLocation)) Directory.CreateDirectory(downloadLocation);

// Download the update
using HttpClient client = new();
using Task<Stream> s = client.GetStreamAsync(ApplicationInfo.IsAppxPackage ? update.DownloadInstallerUrl : update.DownloadPortableUrl);
using FileStream fs = new(downloadFile, FileMode.OpenOrCreate);
s.Result.CopyTo(fs);

// Release all file handles
fs.Close();

// Process the update
if (ApplicationInfo.IsAppxPackage) // Update is a appxbundle
{
// Create a new shell process
ProcessStartInfo psInfo = new()
{
FileName = downloadFile,
UseShellExecute = true
};

// Start the shell process
Process.Start(psInfo);

// Close this application
Environment.Exit(0);
}
else // Update is a zip archive
{
// Extract the archive
ZipFile.ExtractToDirectory(downloadFile, downloadLocation, true);

// Run the new executable to update the original executable
ProcessStartInfo psInfo = new()
{
FileName = Path.Join(downloadLocation, "Minesweeper.exe"),
Arguments = "--update",
WorkingDirectory = downloadLocation,
UseShellExecute = true
};

Process.Start(psInfo);

// Close this application
Environment.Exit(0);
}
}

/// <summary>
/// Apply an update package (.zip archive)
/// </summary>
/// <param name="updatePackage">The path to the update package.</param>
/// <param name="destination">The destination path to use for installation.</param>
/// <exception cref="FileNotFoundException">Invalid update package</exception>
public static void ApplyUpdatePackage(string updatePackage, string destination)
{
// Check if the update package exists
if (!File.Exists(updatePackage))
throw new FileNotFoundException("Invalid update package");

// Apply update package
ZipFile.ExtractToDirectory(updatePackage, destination, true);

// Start the updated application
ProcessStartInfo psInfo = new()
{
FileName = Path.Join(destination, "Minesweeper.exe"),
WorkingDirectory = destination,
UseShellExecute = true
};

Process.Start(psInfo);

// Close this application
Environment.Exit(0);
}
}
4 changes: 4 additions & 0 deletions Minesweeper/Minesweeper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

<ItemGroup>
<Compile Update="Properties\Languages\de_DE.Designer.cs">
<DesignTime>True</DesignTime>
Expand Down
69 changes: 69 additions & 0 deletions Minesweeper/Models/UpdateModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Minesweeper.Utils.Helpers;

namespace Minesweeper.Models;

/// <summary>
/// Stores information about a application update.
/// </summary>
public class UpdateModel
{
/// <summary>
/// The update id provided by GitHub.
/// </summary>
public required int Id { get; init; }

/// <summary>
/// The update version.
/// </summary>
public required (int major, int minor, int patch) Version { get; init; }

/// <summary>
/// The update name / title.
/// </summary>
public required string Name { get; init; }

/// <summary>
/// The update text / body.
/// </summary>
public required string Body { get; init; }

/// <summary>
/// The author of the update.
/// </summary>
public required string Author { get; init; }

/// <summary>
/// Whether this update is a pre release.
/// </summary>
public required bool IsPreRelease { get; init; }

/// <summary>
/// Whether this update is a draft.
/// </summary>
public required bool IsDraft { get; init; }

/// <summary>
/// The update html url.
/// </summary>
public required string Url { get; init; }

/// <summary>
/// The download url for the zip-Archive.
/// </summary>
public required string? DownloadPortableUrl { get; init; }

/// <summary>
/// The download url for the appxbundle-Package
/// </summary>
public required string? DownloadInstallerUrl { get; init; }

/// <summary>
/// Check if this update is newer than the current application version.
/// </summary>
/// <returns><see langword="true" /> if this update is newer; otherwise <see langword="false" />.</returns>
public bool IsNewerVersion()
{
var (currentMajor, currentMinor, currentPatch) = UtilsClass.GetVersion(Application.ProductVersion);
return currentMajor < Version.major || currentMinor < Version.minor || currentPatch < Version.patch;
}
}
34 changes: 34 additions & 0 deletions Minesweeper/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Minesweeper.Controllers;
using Minesweeper.Models;
using System.Runtime;

namespace Minesweeper;
Expand Down Expand Up @@ -31,6 +32,20 @@ private static void Main()
/// </summary>
private static void Run()
{
// Get command line arguments
string[] args = Environment.GetCommandLineArgs();

// Check if this application was launched with the "--update" argument
if (args.Length > 1 && args[1] == "--update")
{
// Apply the update package
UpdateController.ApplyUpdatePackage(Path.Join(Directory.GetCurrentDirectory(), "Minesweeper.zip"), Path.Join(Directory.GetCurrentDirectory(), ".."));
}

// Cleanup unused update directory
if (Directory.Exists(Path.Join(ApplicationInfo.StorageLocation, "tmp_update")))
Directory.Delete(Path.Join(ApplicationInfo.StorageLocation, "tmp_update"), true);

// Show a notification when this application may be installed as an .appx application
if (!ApplicationInfo.IsAppxPackage && ApplicationInfo.MaybeAppxPackage)
{
Expand Down Expand Up @@ -64,6 +79,25 @@ private static void Run()
// Apply user selected language
LanguageController.SetLanguage(Properties.Settings.Default.Language);

// Check for new updates using the GitHub api
try
{
UpdateModel update = UpdateController.CheckForUpdate();

if (update.IsNewerVersion())
{
DialogResult res = MessageBox.Show(
string.Format(LanguageController.CurrentLanguageResource.AppUpdateText, update.Body),
LanguageController.CurrentLanguageResource.AppUpdateTitle, MessageBoxButtons.YesNo,
MessageBoxIcon.Information);
if (res == DialogResult.Yes) UpdateController.InstallUpdate(update);
}
}
catch (Exception)
{
// ignored, because no internet is available
}

// Run application
Application.Run(new Minesweeper());
}
Expand Down
24 changes: 24 additions & 0 deletions Minesweeper/Properties/Languages/de_DE.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Minesweeper/Properties/Languages/de_DE.resx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@
<data name="AppTitleWelcome" xml:space="preserve">
<value>Willkommen zu {0}</value>
</data>
<data name="AppUpdateText" xml:space="preserve">
<value>Eine neue Version steht zum Download bereit.

---
{0}
---

Jetzt installieren?</value>
</data>
<data name="AppUpdateTitle" xml:space="preserve">
<value>Update verfügbar</value>
</data>
<data name="AppVersion" xml:space="preserve">
<value>Version: {0}</value>
</data>
Expand Down
Loading

0 comments on commit 2c10e5d

Please sign in to comment.