1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00
Artemis/src/Artemis.UI/Services/UpdateService.cs
Robert 96c55e5c03 Profile adaption - Added support for device hint to 'all' devices
Layouts - Round LED sizes to integers
Auto-update - Use modern Windows 10 toasts
UI - On shutdown wait 6 seconds longer before force-shutdown
UI - Use black icons in notifications on white Windows theme
Message service - Added optional toast callbacks
2021-05-14 21:49:10 +02:00

264 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Timers;
using Windows.UI.Notifications;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.Settings.Dialogs;
using Artemis.UI.Services.Models.UpdateService;
using Artemis.UI.Shared.Services;
using Flurl;
using Flurl.Http;
using Microsoft.Toolkit.Uwp.Notifications;
using Serilog;
using File = System.IO.File;
namespace Artemis.UI.Services
{
public class UpdateService : IUpdateService
{
private const double UpdateCheckInterval = 3600000; // once per hour
private const string ApiUrl = "https://dev.azure.com/artemis-rgb/Artemis/_apis/";
private readonly PluginSetting<bool> _autoInstallUpdates;
private readonly PluginSetting<bool> _checkForUpdates;
private readonly IDialogService _dialogService;
private readonly ILogger _logger;
private readonly IWindowService _windowService;
public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IWindowService windowService)
{
_logger = logger;
_dialogService = dialogService;
_windowService = windowService;
_windowService.MainWindowOpened += WindowServiceOnMainWindowOpened;
_checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true);
_autoInstallUpdates = settingsService.GetSetting("UI.AutoInstallUpdates", false);
_checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged;
Timer timer = new(UpdateCheckInterval);
timer.Elapsed += async (_, _) => await AutoUpdate();
timer.Start();
}
private async Task OfferUpdate(DevOpsBuild buildInfo)
{
object result = await _dialogService.ShowDialog<UpdateDialogViewModel>(new Dictionary<string, object> {{"buildInfo", buildInfo}});
if (result is bool resultBool && resultBool == false)
SuspendAutoUpdate = true;
}
private async Task UpdateInstaller()
{
string downloadUrl = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe";
string installerDirectory = Path.Combine(Constants.DataFolder, "installer");
string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe");
_logger.Information("UpdateInstaller: Downloading installer from {downloadUrl}", downloadUrl);
using HttpClient client = new();
HttpResponseMessage httpResponseMessage = await client.GetAsync(downloadUrl);
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 async void TOnActivated(ToastNotification sender, object args)
{
if (args is not ToastActivatedEventArgs toastEventArgs)
return;
if (toastEventArgs.Arguments == "update")
await ApplyUpdate();
else if (toastEventArgs.Arguments == "later")
SuspendAutoUpdate = true;
}
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();
}
public bool SuspendAutoUpdate { get; set; }
public async Task<bool> AutoUpdate()
{
if (Constants.BuildInfo.IsLocalBuild)
return false;
if (!_checkForUpdates.Value || SuspendAutoUpdate)
return false;
try
{
return await OfferUpdateIfFound();
}
catch (Exception e)
{
_logger.Warning(e, "Auto update failed");
return false;
}
}
public async Task<bool> OfferUpdateIfFound()
{
_logger.Information("Checking for updates");
DevOpsBuild buildInfo = await GetBuildInfo(1);
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);
if (buildNumber <= Constants.BuildInfo.BuildNumber)
return false;
if (_windowService.IsMainWindowOpen)
await OfferUpdate(buildInfo);
else if (_autoInstallUpdates.Value)
{
new ToastContentBuilder()
.AddText("Installing new version", AdaptiveTextStyle.Header)
.AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.")
.AddProgressBar(null, null, true)
.Show();
await ApplyUpdate();
}
else
{
// If auto-install is disabled and the window is closed, best we can do is notify the user and stop.
new ToastContentBuilder()
.AddText("New version available", AdaptiveTextStyle.Header)
.AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.")
.AddButton("Update", ToastActivationType.Background, "update")
.AddButton("Later", ToastActivationType.Background, "later")
.Show(t => t.Activated += TOnActivated);
}
return true;
}
public async Task<bool> IsUpdateAvailable()
{
DevOpsBuild buildInfo = await GetBuildInfo(1);
double buildNumber = double.Parse(buildInfo.BuildNumber, CultureInfo.InvariantCulture);
return buildNumber > Constants.BuildInfo.BuildNumber;
}
public async Task ApplyUpdate()
{
_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 (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;
}
}
public async Task<DevOpsBuild> GetBuildInfo(int buildDefinition, string buildNumber = null)
{
Url request = ApiUrl.AppendPathSegments("build", "builds")
.SetQueryParam("definitions", buildDefinition)
.SetQueryParam("resultFilter", "succeeded")
.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>();
}
}
public interface IUpdateService : IArtemisUIService
{
bool SuspendAutoUpdate { get; set; }
/// <summary>
/// If auto-update is enabled this will offer updates if found
/// </summary>
Task<bool> AutoUpdate();
Task<bool> OfferUpdateIfFound();
Task<bool> IsUpdateAvailable();
Task<DevOpsBuild> GetBuildInfo(int buildDefinition, string buildNumber = null);
Task<GitHubDifference> GetBuildDifferences(DevOpsBuild a, DevOpsBuild b);
Task ApplyUpdate();
}
}