mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +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="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
|
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
|
||||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
<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" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
<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>
|
/// <summary>
|
||||||
/// Gets or sets a boolean indicating whether auto-updating is suspended.
|
/// Gets or sets a boolean indicating whether auto-updating is suspended.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool SuspendAutoUpdate { get; set; }
|
bool SuspendAutoCheck { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manually checks for updates and offers to install it if found.
|
/// 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 {
|
query GetReleaseById($id: String!) {
|
||||||
release(id: "63b71dd69a5bb32a0a81a410") {
|
release(id: $id) {
|
||||||
branch
|
branch
|
||||||
commit
|
commit
|
||||||
version
|
version
|
||||||
|
previousRelease
|
||||||
artifacts {
|
artifacts {
|
||||||
platform
|
platform
|
||||||
artifactId
|
artifactId
|
||||||
fileInfo {
|
fileInfo {
|
||||||
|
...fileInfo
|
||||||
|
}
|
||||||
|
deltaFileInfo {
|
||||||
|
...fileInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment fileInfo on ArtifactFileInfo {
|
||||||
md5Hash
|
md5Hash
|
||||||
downloadSize
|
downloadSize
|
||||||
downloads
|
}
|
||||||
}
|
|
||||||
}
|
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