mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 21:38:38 +00:00
Updating - Initial client code
This commit is contained in:
parent
3222eae876
commit
a5d2249593
@ -1,31 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a provider for custom cursors.
|
||||
/// </summary>
|
||||
public interface IUpdateProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously checks whether an update is available.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
|
||||
/// <returns>A task returning <see langword="true" /> if an update is available; otherwise <see langword="false" />.</returns>
|
||||
Task<bool> CheckForUpdate(string channel);
|
||||
|
||||
/// <summary>
|
||||
/// Applies any available updates.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
|
||||
/// <param name="silent">Whether or not to update silently.</param>
|
||||
Task ApplyUpdate(string channel, bool silent);
|
||||
|
||||
/// <summary>
|
||||
/// Offer to install the update to the user.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
|
||||
/// <param name="windowOpen">A boolean indicating whether the main window is open.</param>
|
||||
/// <returns>A task returning <see langword="true" /> if the user chose to update; otherwise <see langword="false" />.</returns>
|
||||
Task OfferUpdate(string channel, bool windowOpen);
|
||||
}
|
||||
@ -1,213 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Shared.Providers;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Artemis.UI.Windows.Models;
|
||||
using Artemis.UI.Windows.Screens.Update;
|
||||
using Avalonia.Threading;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Serilog;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Artemis.UI.Windows.Providers;
|
||||
|
||||
public class UpdateProvider : IUpdateProvider, IDisposable
|
||||
{
|
||||
private const string API_URL = "https://dev.azure.com/artemis-rgb/Artemis/_apis/";
|
||||
private const string INSTALLER_URL = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMainWindowService _mainWindowService;
|
||||
private readonly IWindowService _windowService;
|
||||
|
||||
public UpdateProvider(ILogger logger, IWindowService windowService, IMainWindowService mainWindowService)
|
||||
{
|
||||
_logger = logger;
|
||||
_windowService = windowService;
|
||||
_mainWindowService = mainWindowService;
|
||||
|
||||
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
|
||||
}
|
||||
|
||||
public async Task<DevOpsBuild?> GetBuildInfo(int buildDefinition, string? buildNumber = null)
|
||||
{
|
||||
Url request = API_URL.AppendPathSegments("build", "builds")
|
||||
.SetQueryParam("definitions", buildDefinition)
|
||||
.SetQueryParam("resultFilter", "succeeded")
|
||||
.SetQueryParam("branchName", "refs/heads/master")
|
||||
.SetQueryParam("$top", 1)
|
||||
.SetQueryParam("api-version", "6.1-preview.6");
|
||||
|
||||
if (buildNumber != null)
|
||||
request = request.SetQueryParam("buildNumber", buildNumber);
|
||||
|
||||
try
|
||||
{
|
||||
DevOpsBuilds result = await request.GetJsonAsync<DevOpsBuilds>();
|
||||
try
|
||||
{
|
||||
return result.Builds.FirstOrDefault();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (FlurlHttpException e)
|
||||
{
|
||||
_logger.Warning("GetBuildInfo: Getting build info, request returned {StatusCode}", e.StatusCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GitHubDifference> GetBuildDifferences(DevOpsBuild a, DevOpsBuild b)
|
||||
{
|
||||
return await "https://api.github.com"
|
||||
.AppendPathSegments("repos", "Artemis-RGB", "Artemis", "compare")
|
||||
.AppendPathSegment(a.SourceVersion + "..." + b.SourceVersion)
|
||||
.WithHeader("User-Agent", "Artemis 2")
|
||||
.WithHeader("Accept", "application/vnd.github.v3+json")
|
||||
.GetJsonAsync<GitHubDifference>();
|
||||
}
|
||||
|
||||
private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e)
|
||||
{
|
||||
ToastArguments args = ToastArguments.Parse(e.Argument);
|
||||
string channel = args.Get("channel");
|
||||
string action = "view-changes";
|
||||
if (args.Contains("action"))
|
||||
action = args.Get("action");
|
||||
|
||||
if (action == "install")
|
||||
await RunInstaller(channel, true);
|
||||
else if (action == "view-changes")
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
_mainWindowService.OpenMainWindow();
|
||||
await OfferUpdate(channel, true);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunInstaller(string channel, bool silent)
|
||||
{
|
||||
_logger.Information("ApplyUpdate: Applying update");
|
||||
|
||||
// Ensure the installer is up-to-date, get installer build info
|
||||
DevOpsBuild? buildInfo = await GetBuildInfo(6);
|
||||
string installerPath = Path.Combine(Constants.DataFolder, "installer", "Artemis.Installer.exe");
|
||||
|
||||
// Always update installer if it is missing ^^
|
||||
if (!File.Exists(installerPath))
|
||||
{
|
||||
await UpdateInstaller();
|
||||
}
|
||||
// Compare the creation date of the installer with the build date and update if needed
|
||||
else
|
||||
{
|
||||
if (buildInfo != null && File.GetLastWriteTime(installerPath) < buildInfo.FinishTime)
|
||||
await UpdateInstaller();
|
||||
}
|
||||
|
||||
_logger.Information("ApplyUpdate: Running installer at {InstallerPath}", installerPath);
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(installerPath, "-autoupdate")
|
||||
{
|
||||
UseShellExecute = true,
|
||||
Verb = "runas"
|
||||
});
|
||||
}
|
||||
catch (Win32Exception e)
|
||||
{
|
||||
if (e.NativeErrorCode == 0x4c7)
|
||||
_logger.Warning("ApplyUpdate: Operation was cancelled, user likely clicked No in UAC dialog");
|
||||
else
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateInstaller()
|
||||
{
|
||||
string installerDirectory = Path.Combine(Constants.DataFolder, "installer");
|
||||
string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe");
|
||||
|
||||
_logger.Information("UpdateInstaller: Downloading installer from {DownloadUrl}", INSTALLER_URL);
|
||||
using HttpClient client = new();
|
||||
HttpResponseMessage httpResponseMessage = await client.GetAsync(INSTALLER_URL);
|
||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||
throw new ArtemisUIException($"Failed to download installer, status code {httpResponseMessage.StatusCode}");
|
||||
|
||||
_logger.Information("UpdateInstaller: Writing installer file to {InstallerPath}", installerPath);
|
||||
if (File.Exists(installerPath))
|
||||
File.Delete(installerPath);
|
||||
|
||||
Core.Utilities.CreateAccessibleDirectory(installerDirectory);
|
||||
await using FileStream fs = new(installerPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await httpResponseMessage.Content.CopyToAsync(fs);
|
||||
}
|
||||
|
||||
private void ShowDesktopNotification(string channel)
|
||||
{
|
||||
new ToastContentBuilder()
|
||||
.AddArgument("channel", channel)
|
||||
.AddText("An update is available")
|
||||
.AddButton(new ToastButton().SetContent("Install").AddArgument("action", "install").SetBackgroundActivation())
|
||||
.AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes"))
|
||||
.Show();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated;
|
||||
ToastNotificationManagerCompat.Uninstall();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CheckForUpdate(string channel)
|
||||
{
|
||||
DevOpsBuild? buildInfo = await GetBuildInfo(1);
|
||||
if (buildInfo == null)
|
||||
return false;
|
||||
|
||||
double buildNumber = double.Parse(buildInfo.BuildNumber, CultureInfo.InvariantCulture);
|
||||
string buildNumberDisplay = buildNumber.ToString(CultureInfo.InvariantCulture);
|
||||
_logger.Information("Latest build is {BuildNumber}, we're running {LocalBuildNumber}", buildNumberDisplay, Constants.BuildInfo.BuildNumberDisplay);
|
||||
|
||||
return buildNumber > Constants.BuildInfo.BuildNumber;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ApplyUpdate(string channel, bool silent)
|
||||
{
|
||||
await RunInstaller(channel, silent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task OfferUpdate(string channel, bool windowOpen)
|
||||
{
|
||||
if (windowOpen)
|
||||
{
|
||||
bool update = await _windowService.ShowDialogAsync<UpdateDialogViewModel, bool>(channel);
|
||||
if (update)
|
||||
await RunInstaller(channel, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowDesktopNotification(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,7 @@
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
||||
<PackageReference Include="Octopus.Octodiff" Version="2.0.100" />
|
||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||
|
||||
56
src/Artemis.UI/Extensions/HttpClientExtensions.cs
Normal file
56
src/Artemis.UI/Extensions/HttpClientExtensions.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Extensions
|
||||
{
|
||||
public static class HttpClientProgressExtensions
|
||||
{
|
||||
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<float>? progress, CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
long? contentLength = response.Content.Headers.ContentLength;
|
||||
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
// no progress... no contentLength... very sad
|
||||
if (progress is null || !contentLength.HasValue)
|
||||
{
|
||||
await download.CopyToAsync(destination, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
// Such progress and contentLength much reporting Wow!
|
||||
Progress<long> progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
|
||||
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
|
||||
|
||||
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
|
||||
}
|
||||
|
||||
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (bufferSize < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||
if (source is null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
if (!source.CanRead)
|
||||
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
|
||||
if (destination == null)
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
if (!destination.CanWrite)
|
||||
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
|
||||
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
totalBytesRead += bytesRead;
|
||||
progress?.Report(totalBytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings.Updating;
|
||||
|
||||
public class UpdateInstallationViewModel : DialogViewModelBase<bool>
|
||||
{
|
||||
private readonly string _nextReleaseId;
|
||||
|
||||
public UpdateInstallationViewModel(string nextReleaseId)
|
||||
{
|
||||
_nextReleaseId = nextReleaseId;
|
||||
}
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Providers;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Avalonia.Threading;
|
||||
using DryIoc;
|
||||
using Serilog;
|
||||
|
||||
namespace Artemis.UI.Services;
|
||||
|
||||
public class UpdateService : IUpdateService
|
||||
{
|
||||
private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour
|
||||
|
||||
private readonly PluginSetting<bool> _autoUpdate;
|
||||
private readonly PluginSetting<bool> _checkForUpdates;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMainWindowService _mainWindowService;
|
||||
private readonly IUpdateProvider? _updateProvider;
|
||||
|
||||
public UpdateService(ILogger logger, IContainer container, ISettingsService settingsService, IMainWindowService mainWindowService)
|
||||
{
|
||||
_logger = logger;
|
||||
_mainWindowService = mainWindowService;
|
||||
|
||||
if (!Constants.BuildInfo.IsLocalBuild)
|
||||
_updateProvider = container.Resolve<IUpdateProvider>(IfUnresolved.ReturnDefault);
|
||||
|
||||
_checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true);
|
||||
_autoUpdate = settingsService.GetSetting("UI.AutoUpdate", false);
|
||||
_checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged;
|
||||
_mainWindowService.MainWindowOpened += WindowServiceOnMainWindowOpened;
|
||||
|
||||
Timer timer = new(UPDATE_CHECK_INTERVAL);
|
||||
timer.Elapsed += TimerOnElapsed;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private async void TimerOnElapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
await AutoUpdate();
|
||||
}
|
||||
|
||||
private async void CheckForUpdatesOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Run an auto-update as soon as the setting gets changed to enabled
|
||||
if (_checkForUpdates.Value)
|
||||
await AutoUpdate();
|
||||
}
|
||||
|
||||
private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e)
|
||||
{
|
||||
await AutoUpdate();
|
||||
}
|
||||
|
||||
private async Task AutoUpdate()
|
||||
{
|
||||
if (_updateProvider == null || !_checkForUpdates.Value || SuspendAutoUpdate)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
bool updateAvailable = await _updateProvider.CheckForUpdate("master");
|
||||
if (!updateAvailable)
|
||||
return;
|
||||
|
||||
// Only offer it once per session
|
||||
SuspendAutoUpdate = true;
|
||||
|
||||
// If the window is open show the changelog, don't auto-update while the user is busy
|
||||
if (_mainWindowService.IsMainWindowOpen)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// Call OpenMainWindow anyway to focus the main window
|
||||
_mainWindowService.OpenMainWindow();
|
||||
await _updateProvider.OfferUpdate("master", true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If the window is closed but auto-update is enabled, update silently
|
||||
if (_autoUpdate.Value)
|
||||
await _updateProvider.ApplyUpdate("master", true);
|
||||
// If auto-update is disabled the update provider can show a notification and handle the rest
|
||||
else
|
||||
await _updateProvider.OfferUpdate("master", false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(e, "Auto update failed");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SuspendAutoUpdate { get; set; }
|
||||
public bool UpdatingSupported => _updateProvider != null;
|
||||
|
||||
public async Task ManualUpdate()
|
||||
{
|
||||
if (_updateProvider == null || !_mainWindowService.IsMainWindowOpen)
|
||||
return;
|
||||
|
||||
bool updateAvailable = await _updateProvider.CheckForUpdate("master");
|
||||
if (!updateAvailable)
|
||||
return;
|
||||
|
||||
await _updateProvider.OfferUpdate("master", true);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public interface IUpdateNotificationProvider
|
||||
{
|
||||
Task ShowNotification(string releaseId);
|
||||
}
|
||||
@ -12,7 +12,7 @@ public interface IUpdateService : IArtemisUIService
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether auto-updating is suspended.
|
||||
/// </summary>
|
||||
bool SuspendAutoUpdate { get; set; }
|
||||
bool SuspendAutoCheck { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manually checks for updates and offers to install it if found.
|
||||
142
src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
Normal file
142
src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.WebClient.Updating;
|
||||
using NoStringEvaluating.Functions.Math;
|
||||
using Octodiff.Core;
|
||||
using Octodiff.Diagnostics;
|
||||
using Serilog;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the installation process of a release
|
||||
/// </summary>
|
||||
public class ReleaseInstaller
|
||||
{
|
||||
private readonly string _releaseId;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Platform _updatePlatform;
|
||||
private readonly string _dataFolder;
|
||||
|
||||
public IProgress<float> OverallProgress { get; } = new Progress<float>();
|
||||
public IProgress<float> StepProgress { get; } = new Progress<float>();
|
||||
|
||||
public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient)
|
||||
{
|
||||
_releaseId = releaseId;
|
||||
_logger = logger;
|
||||
_updatingClient = updatingClient;
|
||||
_httpClient = httpClient;
|
||||
_dataFolder = Path.Combine(Constants.DataFolder, "updating");
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
_updatePlatform = Platform.Windows;
|
||||
if (OperatingSystem.IsLinux())
|
||||
_updatePlatform = Platform.Linux;
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
_updatePlatform = Platform.Osx;
|
||||
else
|
||||
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
|
||||
|
||||
if (!Directory.Exists(_dataFolder))
|
||||
Directory.CreateDirectory(_dataFolder);
|
||||
}
|
||||
|
||||
public async Task InstallAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
OverallProgress.Report(0);
|
||||
|
||||
_logger.Information("Retrieving details for release {ReleaseId}", _releaseId);
|
||||
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
|
||||
result.EnsureNoErrors();
|
||||
|
||||
IGetReleaseById_Release? release = result.Data?.Release;
|
||||
if (release == null)
|
||||
throw new Exception($"Could not find release with ID {_releaseId}");
|
||||
|
||||
IGetReleaseById_Release_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform);
|
||||
if (artifact == null)
|
||||
throw new Exception("Found the release but it has no artifact for the current platform");
|
||||
|
||||
OverallProgress.Report(0.1f);
|
||||
|
||||
// Determine whether the last update matches our local version, then we can download the delta
|
||||
if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0)
|
||||
await DownloadDelta(artifact, Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip"), cancellationToken);
|
||||
else
|
||||
await Download(artifact, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task DownloadDelta(IGetReleaseById_Release_Artifacts artifact, string previousRelease, CancellationToken cancellationToken)
|
||||
{
|
||||
await using MemoryStream stream = new();
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken);
|
||||
|
||||
OverallProgress.Report(0.33f);
|
||||
|
||||
await PatchDelta(stream, previousRelease, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PatchDelta(MemoryStream deltaStream, string previousRelease, CancellationToken cancellationToken)
|
||||
{
|
||||
await using FileStream baseStream = File.OpenRead(previousRelease);
|
||||
await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
|
||||
deltaStream.Seek(0, SeekOrigin.Begin);
|
||||
DeltaApplier deltaApplier = new();
|
||||
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
OverallProgress.Report(0.66f);
|
||||
await Extract(newFileStream, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task Download(IGetReleaseById_Release_Artifacts artifact, CancellationToken cancellationToken)
|
||||
{
|
||||
await using MemoryStream stream = new();
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken);
|
||||
|
||||
OverallProgress.Report(0.5f);
|
||||
}
|
||||
|
||||
private async Task Extract(FileStream archiveStream, CancellationToken cancellationToken)
|
||||
{
|
||||
// Ensure the directory is empty
|
||||
string extractDirectory = Path.Combine(_dataFolder, "pending");
|
||||
if (Directory.Exists(extractDirectory))
|
||||
Directory.Delete(extractDirectory, true);
|
||||
Directory.CreateDirectory(extractDirectory);
|
||||
|
||||
archiveStream.Seek(0, SeekOrigin.Begin);
|
||||
using ZipArchive archive = new(archiveStream);
|
||||
archive.ExtractToDirectory(extractDirectory);
|
||||
OverallProgress.Report(1);
|
||||
}
|
||||
}
|
||||
|
||||
internal class DeltaApplierProgressReporter : IProgressReporter
|
||||
{
|
||||
private readonly IProgress<float> _stepProgress;
|
||||
|
||||
public DeltaApplierProgressReporter(IProgress<float> stepProgress)
|
||||
{
|
||||
_stepProgress = stepProgress;
|
||||
}
|
||||
|
||||
public void ReportProgress(string operation, long currentPosition, long total)
|
||||
{
|
||||
_stepProgress.Report((float) currentPosition / total);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider
|
||||
{
|
||||
|
||||
}
|
||||
147
src/Artemis.UI/Services/Updating/UpdateService.cs
Normal file
147
src/Artemis.UI/Services/Updating/UpdateService.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Screens.Settings.Updating;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Services.Updating;
|
||||
using Artemis.UI.Shared.Providers;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Artemis.WebClient.Updating;
|
||||
using Avalonia.Threading;
|
||||
using DryIoc;
|
||||
using Serilog;
|
||||
using StrawberryShake;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Artemis.UI.Services;
|
||||
|
||||
public class UpdateService : IUpdateService
|
||||
{
|
||||
private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMainWindowService _mainWindowService;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||
private readonly Func<string, ReleaseInstaller> _getReleaseInstaller;
|
||||
private readonly Platform _updatePlatform;
|
||||
private readonly PluginSetting<string> _channel;
|
||||
private readonly PluginSetting<bool> _autoCheck;
|
||||
private readonly PluginSetting<bool> _autoInstall;
|
||||
|
||||
private bool _suspendAutoCheck;
|
||||
|
||||
public UpdateService(ILogger logger,
|
||||
ISettingsService settingsService,
|
||||
IMainWindowService mainWindowService,
|
||||
IWindowService windowService,
|
||||
IUpdatingClient updatingClient,
|
||||
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
||||
Func<string, ReleaseInstaller> getReleaseInstaller)
|
||||
{
|
||||
_logger = logger;
|
||||
_mainWindowService = mainWindowService;
|
||||
_windowService = windowService;
|
||||
_updatingClient = updatingClient;
|
||||
_updateNotificationProvider = updateNotificationProvider;
|
||||
_getReleaseInstaller = getReleaseInstaller;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
_updatePlatform = Platform.Windows;
|
||||
if (OperatingSystem.IsLinux())
|
||||
_updatePlatform = Platform.Linux;
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
_updatePlatform = Platform.Osx;
|
||||
else
|
||||
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
|
||||
|
||||
_channel = settingsService.GetSetting("UI.Updating.Channel", "master");
|
||||
_autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
||||
_autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", false);
|
||||
_autoCheck.SettingChanged += HandleAutoUpdateEvent;
|
||||
_mainWindowService.MainWindowOpened += HandleAutoUpdateEvent;
|
||||
Timer timer = new(UPDATE_CHECK_INTERVAL);
|
||||
timer.Elapsed += HandleAutoUpdateEvent;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckForUpdate()
|
||||
{
|
||||
string? currentVersion = AssemblyProductVersion;
|
||||
if (currentVersion == null)
|
||||
return false;
|
||||
|
||||
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform);
|
||||
result.EnsureNoErrors();
|
||||
|
||||
// No update was found
|
||||
if (result.Data?.NextRelease == null)
|
||||
return false;
|
||||
|
||||
// Only offer it once per session
|
||||
_suspendAutoCheck = true;
|
||||
|
||||
// If the window is open show the changelog, don't auto-update while the user is busy
|
||||
if (_mainWindowService.IsMainWindowOpen)
|
||||
await ShowUpdateDialog(result.Data.NextRelease.Id);
|
||||
else if (!_autoInstall.Value)
|
||||
await ShowUpdateNotification(result.Data.NextRelease.Id);
|
||||
else
|
||||
await AutoInstallUpdate(result.Data.NextRelease.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ShowUpdateDialog(string nextReleaseId)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// Main window is probably already open but this will bring it into focus
|
||||
_mainWindowService.OpenMainWindow();
|
||||
await _windowService.ShowDialogAsync<UpdateInstallationViewModel>(nextReleaseId);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ShowUpdateNotification(string nextReleaseId)
|
||||
{
|
||||
await _updateNotificationProvider.Value.ShowNotification(nextReleaseId);
|
||||
}
|
||||
|
||||
private async Task AutoInstallUpdate(string nextReleaseId)
|
||||
{
|
||||
ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId);
|
||||
await installer.InstallAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_autoCheck.Value || _suspendAutoCheck)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await CheckForUpdate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Auto update failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? AssemblyProductVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
|
||||
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,30 @@
|
||||
query GetReleaseById {
|
||||
release(id: "63b71dd69a5bb32a0a81a410") {
|
||||
query GetReleaseById($id: String!) {
|
||||
release(id: $id) {
|
||||
branch
|
||||
commit
|
||||
version
|
||||
previousRelease
|
||||
artifacts {
|
||||
platform
|
||||
artifactId
|
||||
fileInfo {
|
||||
md5Hash
|
||||
downloadSize
|
||||
downloads
|
||||
...fileInfo
|
||||
}
|
||||
deltaFileInfo {
|
||||
...fileInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment fileInfo on ArtifactFileInfo {
|
||||
md5Hash
|
||||
downloadSize
|
||||
}
|
||||
|
||||
query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) {
|
||||
nextRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
||||
id
|
||||
version
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user