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 _autoInstallUpdates; private readonly PluginSetting _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(new Dictionary {{"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 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 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 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 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(); 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 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(); } } public interface IUpdateService : IArtemisUIService { bool SuspendAutoUpdate { get; set; } /// /// If auto-update is enabled this will offer updates if found /// Task AutoUpdate(); Task OfferUpdateIfFound(); Task IsUpdateAvailable(); Task GetBuildInfo(int buildDefinition, string buildNumber = null); Task GetBuildDifferences(DevOpsBuild a, DevOpsBuild b); Task ApplyUpdate(); } }