Improve update procedure (#496)

* Improve update procedure

* Change release procedure

* Fix gh release

* Fix release tag

* Change release structure to support folders

* Revert zip file changes to make them backwards compatible

* Change path

* Make launcher not contain the entire dependencies

* Add downtime after API rate limit

* Add rate limit checking before create release

* Enumerate through rates

* Adjust rate limit call

* Change to create release as draft

* Add update logic based on azure blobs

* Fix publish

* Create zip

* Disable CD pipeline for prs

* Increment version

* Fix tests
This commit is contained in:
2023-11-22 21:13:01 +01:00
committed by GitHub
parent 8b9fdaaa30
commit b1a7d3a111
12 changed files with 304 additions and 46 deletions
+17 -3
View File
@@ -83,7 +83,7 @@ jobs:
echo "::set-env name=Version::$version"
- name: Create publish launcher files
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:PublishTrimmed --self-contained true -o .\Publish
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
@@ -93,7 +93,7 @@ jobs:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Create publish extractor files
run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained true -o .\Publish
run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
@@ -103,7 +103,13 @@ jobs:
.\Scripts\BuildRelease.ps1 -version $env:Version
shell: pwsh
- name: Publish release
- name: Publish blob files
run: |
Write-Host $env
.\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}"
shell: pwsh
- name: Create release draft
uses: Xotl/cool-github-releases@v1.1.8
with:
mode: update
@@ -113,6 +119,14 @@ jobs:
github_token: ${{ env.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: ${{ env.Changelog }}
isDraft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release
run: |
gh release edit v${{ env.Version }} --draft=false
shell: powershell
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -1
View File
@@ -5,7 +5,11 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86;x64</Platforms>
<Platforms>AnyCPU;x86</Platforms>
<PublishSingleFile>True</PublishSingleFile>
<Self-Contained>True</Self-Contained>
<PublishReadyToRun>True</PublishReadyToRun>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
</Project>
+97 -24
View File
@@ -1,33 +1,116 @@
// See https://aka.ms/new-console-template for more information
using System.Diagnostics;
using System.IO.Compression;
using System.Text;
static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
{
Console.CursorLeft = 0; // Reset cursor position to the start of the line
Console.Write("["); // Start of the progress bar
double pctComplete = (double)currentStep / totalSteps;
int chars = (int)Math.Round(pctComplete * barSize);
Console.Write(new string('=', chars)); // The 'completed' part of the bar
Console.Write(new string(' ', barSize - chars)); // The 'remaining' part of the bar
Console.Write("] ");
Console.Write($"{pctComplete:P0}"); // Display the percentage completed
}
const string tempFile = "tempfile.zip";
const string updatePkg = "update.pkg";
const string executableName = "Daybreak.exe";
Console.Title = "Daybreak Installer";
Console.WriteLine("Starting installation...");
if (File.Exists(tempFile) is false)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Unable to find launcher package. Aborting installation");
Console.ReadKey();
return;
}
while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDefault()?.HasExited is false)
{
Console.WriteLine($"Detected Daybreak process is still running. Waiting 5s and retrying");
await Task.Delay(5000);
}
Console.WriteLine("Unpacking files...");
try
{
ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true);
}
catch
if (File.Exists(updatePkg))
{
Console.WriteLine("Unpacking files...");
using var fileStream = new FileStream(updatePkg, FileMode.Open);
var sizeBuffer = new byte[4];
var copyBuffer = new byte[1024];
while (fileStream.Position < fileStream.Length - 1)
{
/*
* For each file downloaded, we write to the package the following:
* Size of the name string
* UTF-8 encoded name string
* Size of the relative path string
* UTF-8 encoded relative path string
* Size of the binary
* Binary data
*/
await fileStream.ReadAsync(sizeBuffer, 0, 4);
var fileNameSize = BitConverter.ToInt32(sizeBuffer, 0);
var fileNameBuffer = new byte[fileNameSize];
await fileStream.ReadAsync(fileNameBuffer, 0, fileNameSize);
var fileName = Encoding.UTF8.GetString(fileNameBuffer);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadAsync(sizeBuffer, 0, 4);
var relativePathSize = BitConverter.ToInt32(sizeBuffer, 0);
var relativePathBuffer = new byte[relativePathSize];
await fileStream.ReadAsync(relativePathBuffer, 0, relativePathSize);
var relativePath = Encoding.UTF8.GetString(relativePathBuffer);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadAsync(sizeBuffer, 0, 4);
var binarySize = BitConverter.ToInt32(sizeBuffer, 0);
var fileInfo = new FileInfo(relativePath);
fileInfo.Directory!.Create();
using var destinationStream = new FileStream(relativePath, FileMode.Create);
while(binarySize > 0)
{
var toRead = Math.Min(binarySize, copyBuffer.Length);
await fileStream.ReadAsync(copyBuffer, 0, toRead);
await destinationStream.WriteAsync(copyBuffer, 0, toRead);
binarySize -= toRead;
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
}
}
fileStream.Close();
fileStream.Dispose();
File.Delete(updatePkg);
}
else if (File.Exists(tempFile))
{
Console.WriteLine("Unpacking files...");
try
{
ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true);
}
catch
{
}
Console.WriteLine("Deleting package");
try
{
File.Delete(tempFile);
}
catch (Exception e)
{
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
}
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Unable to find launcher package. Aborting installation");
Console.ReadKey();
return;
}
Console.WriteLine("Deleting browser caches");
@@ -46,16 +129,6 @@ catch(Exception)
{
}
Console.WriteLine("Deleting package");
try
{
File.Delete(tempFile);
}
catch (Exception e)
{
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
}
Console.WriteLine("Launching application");
var process = new Process
{
+1
View File
@@ -4,6 +4,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Platforms>AnyCPU;x86;x64</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
+2 -1
View File
@@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{41AE
Scripts\BuildRelease.ps1 = Scripts\BuildRelease.ps1
Scripts\CompareVersions.ps1 = Scripts\CompareVersions.ps1
Scripts\GetBuildVersion.ps1 = Scripts\GetBuildVersion.ps1
Scripts\PushFilesToBlobStorage.ps1 = Scripts\PushFilesToBlobStorage.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0E30DDA0-2111-4E6C-851E-01DC34124FC2}"
@@ -31,7 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\CODEOWNERS = .github\CODEOWNERS
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Daybreak.7ZipExtractor", "Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj", "{CA60F9AC-A496-4DB6-970E-ECE73B051C05}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Daybreak.7ZipExtractor", "Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj", "{CA60F9AC-A496-4DB6-970E-ECE73B051C05}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -49,4 +49,8 @@ public sealed class LauncherOptions
[JsonProperty(nameof(PersistentLogging))]
[OptionName(Name = "Persistent Logging", Description = "If true, the launcher will save logs in the local database. Otherwise, the launcher will only keep logs in a memory cache")]
public bool PersistentLogging { get; set; } = false;
[JsonProperty(nameof(BetaUpdate))]
[OptionName(Name = "Beta Update", Description = "If true, the launcher will use the new update procedure")]
public bool BetaUpdate { get; set; } = false;
}
+2 -2
View File
@@ -7,16 +7,16 @@
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<Platforms>AnyCPU;x86;x64</Platforms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.152</Version>
<Version>0.9.8.153</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
+111 -7
View File
@@ -4,8 +4,11 @@ using Daybreak.Models.Github;
using Daybreak.Models.Progress;
using Daybreak.Services.Downloads;
using Daybreak.Services.Notifications;
using Daybreak.Services.Privilege;
using Daybreak.Services.Registry;
using Daybreak.Services.Updater.Models;
using Daybreak.Services.Updater.PostUpdate;
using Daybreak.Views;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -13,11 +16,16 @@ using System.Configuration;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static Daybreak.Utils.NativeMethods;
using Version = Daybreak.Models.Versioning.Version;
namespace Daybreak.Services.Updater;
@@ -28,12 +36,15 @@ internal sealed class ApplicationUpdater : IApplicationUpdater
private const string UpdatedKey = "LauncherUpdating";
private const string TempFile = "tempfile.zip";
private const string VersionTag = "{VERSION}";
private const string FileTag = "{FILE}";
private const string RefTagPrefix = "/refs/tags";
private const string VersionListUrl = "https://api.github.com/repos/AlexMacocian/Daybreak/git/refs/tags";
private const string Url = "https://github.com/AlexMacocian/Daybreak/releases/latest";
private const string DownloadUrl = $"https://github.com/AlexMacocian/Daybreak/releases/download/{VersionTag}/Daybreak{VersionTag}.zip";
private const string BlobStorageUrl = $"https://daybreak.blob.core.windows.net/{VersionTag}/{FileTag}";
private readonly CancellationTokenSource updateCancellationTokenSource = new();
private readonly IPrivilegeManager privilegeManager;
private readonly INotificationService notificationService;
private readonly IRegistryService registryService;
private readonly IDownloadService downloadService;
@@ -45,6 +56,7 @@ internal sealed class ApplicationUpdater : IApplicationUpdater
public Version CurrentVersion { get; }
public ApplicationUpdater(
IPrivilegeManager privilegeManager,
INotificationService notificationService,
IRegistryService registryService,
IDownloadService downloadService,
@@ -53,6 +65,7 @@ internal sealed class ApplicationUpdater : IApplicationUpdater
IHttpClient<ApplicationUpdater> httpClient,
ILogger<ApplicationUpdater> logger)
{
this.privilegeManager = privilegeManager.ThrowIfNull();
this.notificationService = notificationService.ThrowIfNull();
this.registryService = registryService.ThrowIfNull();
this.downloadService = downloadService.ThrowIfNull();
@@ -78,17 +91,36 @@ internal sealed class ApplicationUpdater : IApplicationUpdater
public async Task<bool> DownloadUpdate(Version version, UpdateStatus updateStatus)
{
updateStatus.CurrentStep = DownloadStatus.InitializingDownload;
var uri = DownloadUrl.Replace(VersionTag, version.ToString());
if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false)
if (!this.privilegeManager.AdminPrivileges)
{
this.logger.LogError("Failed to download update file");
this.privilegeManager.RequestAdminPrivileges<LauncherView>("Daybreak needs Administrator privileges in order to update");
return false;
}
updateStatus.CurrentStep = UpdateStatus.PendingRestart;
this.logger.LogInformation("Downloaded update file");
return true;
if (version.HasPrefix is false)
{
version.HasPrefix = true;
}
if (!this.liveOptions.Value.BetaUpdate)
{
return await this.DownloadUpdateInternalLegacy(version, updateStatus);
}
var maybeMetadataResponse = await this.httpClient.GetAsync(
BlobStorageUrl
.Replace(VersionTag, version.ToString().Replace(".", "-"))
.Replace(FileTag, "Metadata.json"));
if (maybeMetadataResponse.IsSuccessStatusCode)
{
var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync<List<Metadata>>();
if (metaData is not null)
{
return await this.DownloadUpdateInternalBlob(metaData, version, updateStatus);
}
}
return await this.DownloadUpdateInternalLegacy(version, updateStatus);
}
public async Task<bool> DownloadLatestUpdate(UpdateStatus updateStatus)
@@ -177,6 +209,78 @@ internal sealed class ApplicationUpdater : IApplicationUpdater
{
}
private async Task<bool> DownloadUpdateInternalBlob(List<Metadata> metadata, Version version, UpdateStatus updateStatus)
{
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.DownloadUpdateInternalBlob), version.ToString());
// Exclude daybreak packed files
var daybreakArchive = $"daybreak{version}.zip";
var filesToDownload = metadata
.Where(m => m.Name != daybreakArchive)
.Where(m =>
{
var fileInfo = new FileInfo(m.RelativePath!);
return !fileInfo.Exists || fileInfo.Length != m.Size;
})
.ToList();
updateStatus.CurrentStep = DownloadStatus.InitializingDownload;
using var packageStream = new FileStream("update.pkg", FileMode.Create);
var downloaded = 0d;
var downloadBuffer = new byte[1024];
var sizeToDownload = (double)filesToDownload.Sum(m => m.Size);
var sw = Stopwatch.StartNew();
foreach (var file in filesToDownload)
{
var downloadUrl = BlobStorageUrl.Replace(VersionTag, version.ToString().Replace('.', '-')).Replace(FileTag, file.RelativePath);
var response = await this.httpClient.GetAsync(downloadUrl);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Error {response.StatusCode} when downloading {file.RelativePath}");
return false;
}
var fileNameBytes = Encoding.UTF8.GetBytes(file.Name!);
var relativePathBytes = Encoding.UTF8.GetBytes(file.RelativePath!);
await packageStream.WriteAsync(BitConverter.GetBytes(fileNameBytes.Length));
await packageStream.WriteAsync(fileNameBytes);
await packageStream.WriteAsync(BitConverter.GetBytes(relativePathBytes.Length));
await packageStream.WriteAsync(relativePathBytes);
await packageStream.WriteAsync(BitConverter.GetBytes(file.Size));
var downloadStream = await response.Content.ReadAsStreamAsync();
var fileSize = file.Size;
while (fileSize > 0)
{
var toGet = Math.Min(fileSize, 1024);
await downloadStream.ReadAsync(downloadBuffer, 0, toGet);
fileSize -= toGet;
await packageStream.WriteAsync(downloadBuffer, 0, toGet);
downloaded += toGet;
var etaMillis = sw.ElapsedMilliseconds * (sizeToDownload - downloaded) / downloaded;
updateStatus.CurrentStep = DownloadStatus.Downloading(downloaded / sizeToDownload, TimeSpan.FromMilliseconds(etaMillis));
}
}
scopedLogger.LogInformation($"Prepared update package at {Path.GetFullPath("update.pkg")}");
updateStatus.CurrentStep = UpdateStatus.PendingRestart;
return true;
}
private async Task<bool> DownloadUpdateInternalLegacy(Version version, UpdateStatus updateStatus)
{
updateStatus.CurrentStep = DownloadStatus.InitializingDownload;
var uri = DownloadUrl.Replace(VersionTag, version.ToString());
if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false)
{
this.logger.LogError("Failed to download update file");
return false;
}
updateStatus.CurrentStep = UpdateStatus.PendingRestart;
this.logger.LogInformation("Downloaded update file");
return true;
}
private async Task<Version?> GetLatestVersion()
{
using var response = await this.httpClient.GetAsync(Url);
@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Daybreak.Services.Updater.Models;
internal sealed class Metadata
{
[JsonProperty(nameof(RelativePath))]
public string? RelativePath { get; set; }
[JsonProperty(nameof(Name))]
public string? Name { get; set; }
[JsonProperty(nameof(Size))]
public int Size { get; set; }
}
+4
View File
@@ -5,4 +5,8 @@
Custom launcher for Guildwars.
Requires webview2 runtime https://go.microsoft.com/fwlink/p/?LinkId=2124703.
Download:
- Go to [Releases](https://github.com/AlexMacocian/Daybreak/releases/latest)
- Download daybreakv[VERSION].zip, where [VERSION] is the version of the release
Please check the [wiki](https://github.com/AlexMacocian/Daybreak/wiki) for project description and features
+27 -8
View File
@@ -3,17 +3,36 @@ Param(
[string]$version
) #end param
function Get-FileMetadata {
param (
[string]$Path
)
$fileInfo = Get-Item $Path
# Temporarily change current location to the folder path
$currentLocation = Get-Location
Set-Location -Path .\Publish
$relativePath = Resolve-Path -Path $Path -Relative
Set-Location -Path $currentLocation
return @{
Name = $fileInfo.Name
Size = $fileInfo.Length
RelativePath = $relativePath.trim(".\\")
}
}
$zipPath = "Publish\daybreakv$version.zip"
Write-Output "Deleting pdb file"
Remove-item .\Publish\Daybreak.pdb
Remove-item .\Publish\Daybreak.Installer.pdb
Remove-item .\Publish\Daybreak.7ZipExtractor.pdb
Move-Item -Path .\Publish\Daybreak.Installer.exe -Destination .\Publish\Daybreak.Installer.Temp.exe
$zipPath = "Publish\daybreakv$version.zip"
Write-Output "Compressing binaries to $zipPath"
Compress-Archive .\Publish\* $zipPath -Force
Write-Output "Cleaning up files"
foreach ($file in Get-ChildItem -Path .\Publish -Exclude *.zip -Recurse -ErrorAction SilentlyContinue)
{
Write-Output "Removing $file"
Remove-item $file -Recurse -Force -ErrorAction SilentlyContinue
}
$files = Get-ChildItem -Path .\Publish -Recurse -File
$metadata = $files | ForEach-Object { Get-FileMetadata $_.FullName }
$json = $metadata | ConvertTo-Json
$json | Out-File -FilePath ".\Publish\Metadata.json"
Write-Host "Metadata written to .\Publish\Metadata.json"
+20
View File
@@ -0,0 +1,20 @@
Param(
[Parameter(Mandatory=$true)]
[string]$version,
[Parameter(Mandatory=$true)]
[string]$connectionString,
[Parameter(Mandatory=$true)]
[string]$sourcePath
)
# Create a new blob container
$containerName = "v$($version.ToLower().Replace('.', '-'))" # Container names must be lowercase
Write-Host "Creating container $($containerName)"
az storage container create --name $containerName --connection-string $connectionString
Write-Host "Uploading files"
# Upload all files from the source path to the blob container
az storage blob upload-batch --destination $containerName --source $sourcePath --connection-string $connectionString
Write-Host "Setting public access to container"
az storage container set-permission --name $containerName --public-access blob --connection-string $connectionString