diff --git a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs b/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs
deleted file mode 100644
index 0c33b135f..000000000
--- a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Artemis.UI.Shared.Providers;
-
-///
-/// Represents a provider for custom cursors.
-///
-public interface IUpdateProvider
-{
- ///
- /// Asynchronously checks whether an update is available.
- ///
- /// The channel to use when checking updates (i.e. master or development)
- /// A task returning if an update is available; otherwise .
- Task CheckForUpdate(string channel);
-
- ///
- /// Applies any available updates.
- ///
- /// The channel to use when checking updates (i.e. master or development)
- /// Whether or not to update silently.
- Task ApplyUpdate(string channel, bool silent);
-
- ///
- /// Offer to install the update to the user.
- ///
- /// The channel to use when checking updates (i.e. master or development)
- /// A boolean indicating whether the main window is open.
- /// A task returning if the user chose to update; otherwise .
- Task OfferUpdate(string channel, bool windowOpen);
-}
\ No newline at end of file
diff --git a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs b/src/Artemis.UI.Windows/Providers/UpdateProvider.cs
deleted file mode 100644
index d3d20081e..000000000
--- a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs
+++ /dev/null
@@ -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 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();
- 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();
- }
-
- 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();
- }
-
- ///
- public void Dispose()
- {
- ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated;
- ToastNotificationManagerCompat.Uninstall();
- }
-
- ///
- public async Task 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;
- }
-
- ///
- public async Task ApplyUpdate(string channel, bool silent)
- {
- await RunInstaller(channel, silent);
- }
-
- ///
- public async Task OfferUpdate(string channel, bool windowOpen)
- {
- if (windowOpen)
- {
- bool update = await _windowService.ShowDialogAsync(channel);
- if (update)
- await RunInstaller(channel, false);
- }
- else
- {
- ShowDesktopNotification(channel);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index 987233c0e..101b1ac8a 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -30,6 +30,7 @@
+
diff --git a/src/Artemis.UI/Extensions/HttpClientExtensions.cs b/src/Artemis.UI/Extensions/HttpClientExtensions.cs
new file mode 100644
index 000000000..50af33443
--- /dev/null
+++ b/src/Artemis.UI/Extensions/HttpClientExtensions.cs
@@ -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? 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 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 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs
new file mode 100644
index 000000000..4b1950846
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs
@@ -0,0 +1,14 @@
+using Artemis.Core;
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.Settings.Updating;
+
+public class UpdateInstallationViewModel : DialogViewModelBase
+{
+ private readonly string _nextReleaseId;
+
+ public UpdateInstallationViewModel(string nextReleaseId)
+ {
+ _nextReleaseId = nextReleaseId;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs
deleted file mode 100644
index ab8e0e2ff..000000000
--- a/src/Artemis.UI/Services/UpdateService.cs
+++ /dev/null
@@ -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 _autoUpdate;
- private readonly PluginSetting _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(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);
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs
new file mode 100644
index 000000000..fcf306bb4
--- /dev/null
+++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace Artemis.UI.Services.Updating;
+
+public interface IUpdateNotificationProvider
+{
+ Task ShowNotification(string releaseId);
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs
similarity index 93%
rename from src/Artemis.UI/Services/Interfaces/IUpdateService.cs
rename to src/Artemis.UI/Services/Updating/IUpdateService.cs
index cc6236853..157d1ef07 100644
--- a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs
+++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs
@@ -12,7 +12,7 @@ public interface IUpdateService : IArtemisUIService
///
/// Gets or sets a boolean indicating whether auto-updating is suspended.
///
- bool SuspendAutoUpdate { get; set; }
+ bool SuspendAutoCheck { get; set; }
///
/// Manually checks for updates and offers to install it if found.
diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
new file mode 100644
index 000000000..f868298b7
--- /dev/null
+++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
@@ -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;
+
+///
+/// Represents the installation process of a release
+///
+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 OverallProgress { get; } = new Progress();
+ public IProgress StepProgress { get; } = new Progress();
+
+ 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 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 _stepProgress;
+
+ public DeltaApplierProgressReporter(IProgress stepProgress)
+ {
+ _stepProgress = stepProgress;
+ }
+
+ public void ReportProgress(string operation, long currentPosition, long total)
+ {
+ _stepProgress.Report((float) currentPosition / total);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs
new file mode 100644
index 000000000..94bcaf3be
--- /dev/null
+++ b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs
@@ -0,0 +1,6 @@
+namespace Artemis.UI.Services.Updating;
+
+public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider
+{
+
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs
new file mode 100644
index 000000000..1077d549c
--- /dev/null
+++ b/src/Artemis.UI/Services/Updating/UpdateService.cs
@@ -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 _updateNotificationProvider;
+ private readonly Func _getReleaseInstaller;
+ private readonly Platform _updatePlatform;
+ private readonly PluginSetting _channel;
+ private readonly PluginSetting _autoCheck;
+ private readonly PluginSetting _autoInstall;
+
+ private bool _suspendAutoCheck;
+
+ public UpdateService(ILogger logger,
+ ISettingsService settingsService,
+ IMainWindowService mainWindowService,
+ IWindowService windowService,
+ IUpdatingClient updatingClient,
+ Lazy updateNotificationProvider,
+ Func 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 CheckForUpdate()
+ {
+ string? currentVersion = AssemblyProductVersion;
+ if (currentVersion == null)
+ return false;
+
+ IOperationResult 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(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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql
index fa17ed4f0..cd7e8b2a8 100644
--- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql
+++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql
@@ -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
+ }
+}
\ No newline at end of file