mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
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
264 lines
10 KiB
C#
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();
|
|
}
|
|
} |