diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 15213165d..252d3b85a 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -48,6 +48,10 @@ public static class Constants /// The full path to the Artemis logs folder /// public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); + /// + /// The full path to the Artemis logs folder + /// + public static readonly string UpdatingFolder = Path.Combine(DataFolder, "updating"); /// /// The full path to the Artemis plugins folder diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index e5590b568..db6e373a0 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -21,6 +21,7 @@ public static class Utilities CreateAccessibleDirectory(Constants.DataFolder); CreateAccessibleDirectory(Constants.PluginsFolder); CreateAccessibleDirectory(Constants.LayoutsFolder); + CreateAccessibleDirectory(Constants.UpdatingFolder); } /// diff --git a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs index 3d10ed71b..7c517ff79 100644 --- a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs +++ b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs @@ -7,15 +7,5 @@ public class ReleaseEntity public Guid Id { get; set; } public string Version { get; set; } - public string ReleaseId { get; set; } - public ReleaseEntityStatus Status { get; set; } public DateTimeOffset? InstalledAt { get; set; } -} - -public enum ReleaseEntityStatus -{ - Queued, - Installed, - Historical, - Unknown } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ReleaseRepository.cs b/src/Artemis.Storage/Repositories/ReleaseRepository.cs index 0c2d466dd..3516c6c80 100644 --- a/src/Artemis.Storage/Repositories/ReleaseRepository.cs +++ b/src/Artemis.Storage/Repositories/ReleaseRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories.Interfaces; using LiteDB; @@ -14,63 +13,25 @@ public class ReleaseRepository : IReleaseRepository { _repository = repository; _repository.Database.GetCollection().EnsureIndex(s => s.Version, true); - _repository.Database.GetCollection().EnsureIndex(s => s.Status); } - public ReleaseEntity GetQueuedVersion() + public void SaveVersionInstallDate(string version) { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).FirstOrDefault(); - } + ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault(); + if (release != null) + return; - public ReleaseEntity GetInstalledVersion() - { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Installed).FirstOrDefault(); + _repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow}); } public ReleaseEntity GetPreviousInstalledVersion() { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Historical).OrderByDescending(r => r.InstalledAt).FirstOrDefault(); - } - - public void QueueInstallation(string version, string releaseId) - { - // Mark release as queued and add if missing - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version, ReleaseId = releaseId}; - release.Status = ReleaseEntityStatus.Queued; - _repository.Upsert(release); - } - - public void FinishInstallation(string version) - { - // Mark release as installed and add if missing - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version}; - release.Status = ReleaseEntityStatus.Installed; - release.InstalledAt = DateTimeOffset.UtcNow; - _repository.Upsert(release); - - // Mark other releases as historical - List oldReleases = _repository.Query().Where(r => r.Version != version && r.Status != ReleaseEntityStatus.Historical).ToList(); - foreach (ReleaseEntity oldRelease in oldReleases) - oldRelease.Status = ReleaseEntityStatus.Historical; - _repository.Update(oldReleases); - } - - public void DequeueInstallation() - { - // Mark all queued releases as unknown, until FinishInstallation is called we don't know the status - List queuedReleases = _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).ToList(); - foreach (ReleaseEntity queuedRelease in queuedReleases) - queuedRelease.Status = ReleaseEntityStatus.Unknown; - _repository.Update(queuedReleases); + return _repository.Query().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault(); } } public interface IReleaseRepository : IRepository { - ReleaseEntity GetQueuedVersion(); - ReleaseEntity GetInstalledVersion(); + void SaveVersionInstallDate(string version); ReleaseEntity GetPreviousInstalledVersion(); - void QueueInstallation(string version, string releaseId); - void FinishInstallation(string version); - void DequeueInstallation(); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index b2a8db3df..9444327c6 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -43,9 +43,9 @@ public class App : Application { if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown) return; - - ArtemisBootstrapper.Initialize(); + _applicationStateManager = new ApplicationStateManager(_container!, desktop.Args); + ArtemisBootstrapper.Initialize(); RegisterProviders(_container!); } diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 1a7d8147e..05435e5ac 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -99,8 +99,8 @@ public class ApplicationStateManager argsList.Add("--autorun"); // Retain startup arguments after update by providing them to the script - string script = $"\"{Path.Combine(Constants.DataFolder, "updating", "pending", "scripts", "update.ps1")}\""; - string source = $"-sourceDirectory \"{Path.Combine(Constants.DataFolder, "updating", "pending")}\""; + string script = $"\"{Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1")}\""; + string source = $"-sourceDirectory \"{Path.Combine(Constants.UpdatingFolder, "installing")}\""; string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index 024844bd6..3b434360d 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -9,6 +9,7 @@ using Artemis.UI.Screens.Settings; using Artemis.UI.Services.Updating; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; +using DryIoc.ImTools; using Microsoft.Toolkit.Uwp.Notifications; using ReactiveUI; @@ -16,7 +17,7 @@ namespace Artemis.UI.Windows.Providers; public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider { - private readonly Func _getReleaseInstaller; + private readonly Func _getReleaseInstaller; private readonly Func _getSettingsViewModel; private readonly IMainWindowService _mainWindowService; private readonly IUpdateService _updateService; @@ -25,7 +26,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, Func getSettingsViewModel, - Func getReleaseInstaller) + Func getReleaseInstaller) { _mainWindowService = mainWindowService; _updateService = updateService; @@ -37,7 +38,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) { ToastArguments args = ToastArguments.Parse(e.Argument); - string releaseId = args.Get("releaseId"); + Guid releaseId = Guid.Parse(args.Get("releaseId")); string releaseVersion = args.Get("releaseVersion"); string action = "view-changes"; if (args.Contains("action")) @@ -53,7 +54,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider _updateService.RestartForUpdate(false); } - public void ShowNotification(string releaseId, string releaseVersion) + public void ShowNotification(Guid releaseId, string releaseVersion) { GetBuilderForRelease(releaseId, releaseVersion) .AddText("Update available") @@ -62,10 +63,10 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider .SetContent("Install") .AddArgument("action", "install").SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate)) .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes")) - .Show(t => t.Tag = releaseId); + .Show(t => t.Tag = releaseId.ToString()); } - private void ViewRelease(string releaseId) + private void ViewRelease(Guid releaseId) { Dispatcher.UIThread.Post(() => { @@ -87,7 +88,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider }); } - private async Task InstallRelease(string releaseId, string releaseVersion) + private async Task InstallRelease(Guid releaseId, string releaseVersion) { ReleaseInstaller installer = _getReleaseInstaller(releaseId); void InstallerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateInstallProgress(releaseId, installer); @@ -104,7 +105,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider .AddButton(new ToastButton().SetContent("Cancel").AddArgument("action", "cancel")) .Show(t => { - t.Tag = releaseId; + t.Tag = releaseId.ToString(); t.Data = GetDataForInstaller(installer); }); @@ -127,26 +128,23 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider installer.PropertyChanged -= InstallerOnPropertyChanged; } - // Queue an update in case the user interrupts the process after everything has been prepared - _updateService.QueueUpdate(releaseVersion, releaseId); - GetBuilderForRelease(releaseId, releaseVersion) .AddAudio(new ToastAudio {Silent = true}) .AddText("Update ready") .AddText($"Artemis version {releaseVersion} is ready to be applied") .AddButton(new ToastButton().SetContent("Restart Artemis").AddArgument("action", "restart-for-update")) .AddButton(new ToastButton().SetContent("Later").AddArgument("action", "postpone-update")) - .Show(t => t.Tag = releaseId); + .Show(t => t.Tag = releaseId.ToString()); } - private void UpdateInstallProgress(string releaseId, ReleaseInstaller installer) + private void UpdateInstallProgress(Guid releaseId, ReleaseInstaller installer) { - ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId); + ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId.ToString()); } - private ToastContentBuilder GetBuilderForRelease(string releaseId, string releaseVersion) + private ToastContentBuilder GetBuilderForRelease(Guid releaseId, string releaseVersion) { - return new ToastContentBuilder().AddArgument("releaseId", releaseId).AddArgument("releaseVersion", releaseVersion); + return new ToastContentBuilder().AddArgument("releaseId", releaseId.ToString()).AddArgument("releaseVersion", releaseVersion); } private NotificationData GetDataForInstaller(ReleaseInstaller installer) diff --git a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs index 8b3c5fa42..7d3e344aa 100644 --- a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs +++ b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs @@ -480,7 +480,7 @@ public class ScriptVmFactory : IScriptVmFactory public interface IReleaseVmFactory : IVmFactory { - ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt); + ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt); } public class ReleaseVmFactory : IReleaseVmFactory { @@ -491,7 +491,7 @@ public class ReleaseVmFactory : IReleaseVmFactory _container = container; } - public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt) + public ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt) { return _container.Resolve(new object[] { releaseId, version, createdAt }); } diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 2037d0b1a..373fd22f1 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -64,6 +64,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel); Task.Run(() => { + if (_updateService.Initialize()) + return; + coreService.Initialize(); registrationService.RegisterBuiltInDataModelDisplays(); registrationService.RegisterBuiltInDataModelInputs(); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs index c504c4510..8d4c54f1c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs @@ -61,7 +61,7 @@ public class ReleasesTabViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection ReleaseViewModels { get; } public string Channel { get; } - public string? PreselectId { get; set; } + public Guid? PreselectId { get; set; } public ReleaseViewModel? SelectedReleaseViewModel { diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs index 263ff055e..bce5e0181 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs @@ -36,7 +36,7 @@ public class ReleaseViewModel : ActivatableViewModelBase private bool _loading = true; private bool _retrievedDetails; - public ReleaseViewModel(string releaseId, + public ReleaseViewModel(Guid releaseId, string version, DateTimeOffset createdAt, ILogger logger, @@ -79,7 +79,7 @@ public class ReleaseViewModel : ActivatableViewModelBase }); } - public string ReleaseId { get; } + public Guid ReleaseId { get; } private void ExecuteRestart() { @@ -158,7 +158,6 @@ public class ReleaseViewModel : ActivatableViewModelBase { InstallationInProgress = true; await ReleaseInstaller.InstallAsync(_installerCts.Token); - _updateService.QueueUpdate(Version, ReleaseId); InstallationFinished = true; } catch (Exception e) diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs index 9e1268f29..5f77faf60 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -1,8 +1,9 @@ +using System; using System.Threading.Tasks; namespace Artemis.UI.Services.Updating; public interface IUpdateNotificationProvider { - void ShowNotification(string releaseId, string releaseVersion); + void ShowNotification(Guid releaseId, string releaseVersion); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 0da40b1a9..225ecbd97 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Artemis.UI.Services.Interfaces; using Artemis.WebClient.Updating; @@ -6,14 +7,47 @@ namespace Artemis.UI.Services.Updating; public interface IUpdateService : IArtemisUIService { + /// + /// Gets the current update channel. + /// string Channel { get; } + + /// + /// Gets the version number of the previous release that was installed, if any. + /// string? PreviousVersion { get; } + + /// + /// The latest cached release, can be updated by calling . + /// IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; } + /// + /// Asynchronously caches the latest release. + /// Task CacheLatestRelease(); - Task CheckForUpdate(); - void QueueUpdate(string version, string releaseId); - ReleaseInstaller GetReleaseInstaller(string releaseId); + /// + /// Asynchronously checks whether an update is available on the current . + /// + Task CheckForUpdate(); + + /// + /// Creates a release installed for a release with the provided ID. + /// + /// The ID of the release to create the installer for. + /// The resulting release installer. + ReleaseInstaller GetReleaseInstaller(Guid releaseId); + + /// + /// Restarts the application to install a pending update. + /// + /// A boolean indicating whether to perform a silent install of the update. void RestartForUpdate(bool silent); + + /// + /// Initializes the update service. + /// + /// A boolean indicating whether a restart will occur to install a pending update. + bool Initialize(); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs index 1a4d066b9..46a06a2ea 100644 --- a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs @@ -23,7 +23,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider _getSettingsViewModel = getSettingsViewModel; } - private void ShowInAppNotification(string releaseId, string releaseVersion) + private void ShowInAppNotification(Guid releaseId, string releaseVersion) { _notification?.Invoke(); _notification = _notificationService.CreateNotification() @@ -35,7 +35,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider .Show(); } - private void ViewRelease(string releaseId) + private void ViewRelease(Guid releaseId) { _notification?.Invoke(); @@ -56,7 +56,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider } /// - public void ShowNotification(string releaseId, string releaseVersion) + public void ShowNotification(Guid releaseId, string releaseVersion) { if (_mainWindowService.IsMainWindowOpen) ShowInAppNotification(releaseId, releaseVersion); diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs index 78535ac31..04ca4f74f 100644 --- a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -22,24 +22,26 @@ namespace Artemis.UI.Services.Updating; /// public class ReleaseInstaller : CorePropertyChanged { - private readonly string _dataFolder; private readonly HttpClient _httpClient; private readonly ILogger _logger; - private readonly string _releaseId; + private readonly Guid _releaseId; private readonly Platform _updatePlatform; private readonly IUpdatingClient _updatingClient; private readonly Progress _progress = new(); + + private IGetReleaseById_PublishedRelease _release = null!; + private IGetReleaseById_PublishedRelease_Artifacts _artifact = null!; + private Progress _stepProgress = new(); private string _status = string.Empty; - private float _progress1; + private float _floatProgress; - public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) + public ReleaseInstaller(Guid 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; @@ -50,9 +52,6 @@ public class ReleaseInstaller : CorePropertyChanged else throw new PlatformNotSupportedException("Cannot auto update on the current platform"); - if (!Directory.Exists(_dataFolder)) - Directory.CreateDirectory(_dataFolder); - _progress.ProgressChanged += (_, f) => Progress = f; } @@ -64,8 +63,8 @@ public class ReleaseInstaller : CorePropertyChanged public float Progress { - get => _progress1; - set => SetAndNotify(ref _progress1, value); + get => _floatProgress; + set => SetAndNotify(ref _floatProgress, value); } public async Task InstallAsync(CancellationToken cancellationToken) @@ -79,24 +78,24 @@ public class ReleaseInstaller : CorePropertyChanged IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); result.EnsureNoErrors(); - IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease; - if (release == null) + _release = result.Data?.PublishedRelease!; + if (_release == null) throw new Exception($"Could not find release with ID {_releaseId}"); - IGetReleaseById_PublishedRelease_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform); - if (artifact == null) + _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"); ((IProgress) _progress).Report(10); // 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); + if (_release.PreviousRelease != null && File.Exists(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip")) && _artifact.DeltaFileInfo.DownloadSize != 0) + await DownloadDelta(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip"), cancellationToken); else - await Download(artifact, cancellationToken); + await Download(cancellationToken); } - private async Task DownloadDelta(IGetReleaseById_PublishedRelease_Artifacts artifact, string previousRelease, CancellationToken cancellationToken) + private async Task DownloadDelta(string previousRelease, CancellationToken cancellationToken) { // 10 - 50% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; @@ -104,20 +103,20 @@ public class ReleaseInstaller : CorePropertyChanged Status = "Downloading..."; await using MemoryStream stream = new(); - await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{_artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken); _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await PatchDelta(stream, previousRelease, artifact, cancellationToken); + await PatchDelta(stream, previousRelease, cancellationToken); } - private async Task PatchDelta(Stream deltaStream, string previousRelease, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken) { // 50 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(50f + e * 0.1f); Status = "Patching..."; - await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await using FileStream newFileStream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); await using (FileStream baseStream = File.OpenRead(previousRelease)) { deltaStream.Seek(0, SeekOrigin.Begin); @@ -132,23 +131,23 @@ public class ReleaseInstaller : CorePropertyChanged _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await ValidateArchive(newFileStream, artifact, cancellationToken); + await ValidateArchive(newFileStream, cancellationToken); await Extract(newFileStream, cancellationToken); } - private async Task Download(IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task Download(CancellationToken cancellationToken) { // 10 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(10f + e * 0.5f); Status = "Downloading..."; - await using FileStream stream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); - await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}", stream, _stepProgress, cancellationToken); + await using FileStream stream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{_artifact.ArtifactId}", stream, _stepProgress, cancellationToken); _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await ValidateArchive(stream, artifact, cancellationToken); + await ValidateArchive(stream, cancellationToken); await Extract(stream, cancellationToken); } @@ -160,7 +159,7 @@ public class ReleaseInstaller : CorePropertyChanged Status = "Extracting..."; // Ensure the directory is empty - string extractDirectory = Path.Combine(_dataFolder, "pending"); + string extractDirectory = Path.Combine(Constants.UpdatingFolder, "pending"); if (Directory.Exists(extractDirectory)) Directory.Delete(extractDirectory, true); Directory.CreateDirectory(extractDirectory); @@ -176,12 +175,12 @@ public class ReleaseInstaller : CorePropertyChanged _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; } - private async Task ValidateArchive(Stream archiveStream, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task ValidateArchive(Stream archiveStream, CancellationToken cancellationToken) { using MD5 md5 = MD5.Create(); archiveStream.Seek(0, SeekOrigin.Begin); string hash = BitConverter.ToString(await md5.ComputeHashAsync(archiveStream, cancellationToken)).Replace("-", ""); - if (hash != artifact.FileInfo.Md5Hash) - throw new ArtemisUIException($"Update file hash mismatch, expected \"{artifact.FileInfo.Md5Hash}\" but got \"{hash}\""); + if (hash != _artifact.FileInfo.Md5Hash) + throw new ArtemisUIException($"Update file hash mismatch, expected \"{_artifact.FileInfo.Md5Hash}\" but got \"{hash}\""); } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 8e46d20b5..70c53e3f5 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -7,6 +7,7 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories; +using Artemis.UI.Exceptions; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; using Serilog; @@ -20,7 +21,7 @@ public class UpdateService : IUpdateService private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour private readonly PluginSetting _autoCheck; private readonly PluginSetting _autoInstall; - private readonly Func _getReleaseInstaller; + private readonly Func _getReleaseInstaller; private readonly ILogger _logger; private readonly IReleaseRepository _releaseRepository; @@ -36,7 +37,7 @@ public class UpdateService : IUpdateService IUpdatingClient updatingClient, IReleaseRepository releaseRepository, Lazy updateNotificationProvider, - Func getReleaseInstaller) + Func getReleaseInstaller) { _logger = logger; _updatingClient = updatingClient; @@ -66,39 +67,21 @@ public class UpdateService : IUpdateService Timer timer = new(UPDATE_CHECK_INTERVAL); timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); - - _logger.Information("Update service initialized for {Channel} channel", Channel); - ProcessReleaseStatus(); } private void ProcessReleaseStatus() { - // If an update is queued, don't bother with anything else - ReleaseEntity? queued = _releaseRepository.GetQueuedVersion(); - if (queued != null) - { - // Remove the queued installation, in case something goes wrong then at least we don't end up in a loop - _logger.Information("Installing queued version {Version}", queued.Version); - RestartForUpdate(true); - return; - } - - // If a different version was installed, mark it as such - ReleaseEntity? installed = _releaseRepository.GetInstalledVersion(); - if (installed?.Version != Constants.CurrentVersion) - _releaseRepository.FinishInstallation(Constants.CurrentVersion); - + string currentVersion = Constants.CurrentVersion; + _releaseRepository.SaveVersionInstallDate(currentVersion); PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; - if (!Directory.Exists(Path.Combine(Constants.DataFolder, "updating"))) + if (!Directory.Exists(Constants.UpdatingFolder)) return; // Clean up the update folder, leaving only the last ZIP - foreach (string file in Directory.GetFiles(Path.Combine(Constants.DataFolder, "updating"))) + foreach (string file in Directory.GetFiles(Constants.UpdatingFolder)) { - if (Path.GetExtension(file) != ".zip") - continue; - if (installed != null && Path.GetFileName(file) == $"{installed.ReleaseId}.zip") + if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip") continue; try @@ -140,8 +123,13 @@ public class UpdateService : IUpdateService } } + /// public string Channel { get; } - public string? PreviousVersion { get; set; } + + /// + public string? PreviousVersion { get; private set; } + + /// public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } /// @@ -158,6 +146,7 @@ public class UpdateService : IUpdateService } } + /// public async Task CheckForUpdate() { IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform); @@ -183,13 +172,7 @@ public class UpdateService : IUpdateService } /// - public void QueueUpdate(string version, string releaseId) - { - _releaseRepository.QueueInstallation(version, releaseId); - } - - /// - public ReleaseInstaller GetReleaseInstaller(string releaseId) + public ReleaseInstaller GetReleaseInstaller(Guid releaseId) { return _getReleaseInstaller(releaseId); } @@ -197,7 +180,33 @@ public class UpdateService : IUpdateService /// public void RestartForUpdate(bool silent) { - _releaseRepository.DequeueInstallation(); + if (!Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending"))) + throw new ArtemisUIException("Cannot install update, none is pending."); + + Directory.Move(Path.Combine(Constants.UpdatingFolder, "pending"), Path.Combine(Constants.UpdatingFolder, "installing")); Utilities.ApplyUpdate(silent); } + + /// + public bool Initialize() + { + // There should never be an installing folder + if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "installing"))) + { + _logger.Warning("Cleaning up leftover installing folder, did an update go wrong?"); + Directory.Delete(Path.Combine(Constants.UpdatingFolder, "installing")); + } + + // If an update is pending, don't bother with anything else + if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending"))) + { + _logger.Information("Installing pending update"); + RestartForUpdate(true); + return true; + } + + ProcessReleaseStatus(); + _logger.Information("Update service initialized for {Channel} channel", Channel); + return false; + } } \ 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 dff2f5582..e364038ff 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -22,12 +22,14 @@ query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: S } -query GetReleaseById($id: String!) { +query GetReleaseById($id: UUID!) { publishedRelease(id: $id) { branch commit version - previousRelease + previousRelease { + version + } changelog artifacts { platform diff --git a/src/Artemis.WebClient.Updating/schema.graphql b/src/Artemis.WebClient.Updating/schema.graphql index 464ffb797..fe855cb6c 100644 --- a/src/Artemis.WebClient.Updating/schema.graphql +++ b/src/Artemis.WebClient.Updating/schema.graphql @@ -5,33 +5,6 @@ schema { mutation: Mutation } -"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." -directive @defer( - "Deferred when true." - if: Boolean, - "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." - label: String -) on FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." -directive @stream( - "Streamed when true." - if: Boolean, - "The initial elements that shall be send down to the consumer." - initialCount: Int! = 0, - "If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." - label: String -) on FIELD - -directive @authorize( - "Defines when when the resolver shall be executed.By default the resolver is executed after the policy has determined that the current user is allowed to access the field." - apply: ApplyPolicy! = BEFORE_RESOLVER, - "The name of the authorization policy that determines access to the annotated resource." - policy: String, - "Roles that are allowed to access the annotated resource." - roles: [String!] -) on SCHEMA | OBJECT | FIELD_DEFINITION - type ArtemisChannel { branch: String! releases: Int! @@ -41,15 +14,25 @@ type Artifact { artifactId: Long! deltaFileInfo: ArtifactFileInfo! fileInfo: ArtifactFileInfo! + id: UUID! platform: Platform! } type ArtifactFileInfo { downloadSize: Long! downloads: Long! + id: UUID! md5Hash: String } +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + type Mutation { updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload! } @@ -74,6 +57,7 @@ type PublishedReleasesConnection { nodes: [Release!] "Information to aid in pagination." pageInfo: PageInfo! + "Identifies the total count of items in the connection." totalCount: Int! } @@ -90,7 +74,7 @@ type Query { channels: [ArtemisChannel!]! nextPublishedRelease(branch: String!, platform: Platform!, version: String): Release publishedChannels: [String!]! - publishedRelease(id: String!): Release + publishedRelease(id: UUID!): Release publishedReleases( "Returns the elements in the list that come after the specified cursor." after: String, @@ -103,20 +87,9 @@ type Query { order: [ReleaseSortInput!], where: ReleaseFilterInput ): PublishedReleasesConnection - release(id: String!): Release + release(id: UUID!): Release releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]! - releases( - "Returns the elements in the list that come after the specified cursor." - after: String, - "Returns the elements in the list that come before the specified cursor." - before: String, - "Returns the first _n_ elements from the list." - first: Int, - "Returns the last _n_ elements from the list." - last: Int, - order: [ReleaseSortInput!], - where: ReleaseFilterInput - ): ReleasesConnection + releases(order: [ReleaseSortInput!], skip: Int, take: Int, where: ReleaseFilterInput): ReleasesCollectionSegment } type Release { @@ -125,9 +98,9 @@ type Release { changelog: String! commit: String! createdAt: DateTime! - id: String! + id: UUID! isDraft: Boolean! - previousRelease: String + previousRelease: Release version: String! workflowRunId: Long! } @@ -136,30 +109,20 @@ type ReleaseStatistic { count: Int! lastReportedUsage: DateTime! linuxCount: Int! - oSXCount: Int! - releaseId: String! + osxCount: Int! + releaseId: UUID! windowsCount: Int! } -"A connection to a list of items." -type ReleasesConnection { - "A list of edges." - edges: [ReleasesEdge!] - "A flattened list of the nodes." - nodes: [Release!] +"A segment of a collection." +type ReleasesCollectionSegment { + "A flattened list of the items." + items: [Release!] "Information to aid in pagination." - pageInfo: PageInfo! + pageInfo: CollectionSegmentInfo! totalCount: Int! } -"An edge in a connection." -type ReleasesEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Release! -} - type UpdateReleaseChangelogPayload { release: Release } @@ -167,6 +130,7 @@ type UpdateReleaseChangelogPayload { enum ApplyPolicy { AFTER_RESOLVER BEFORE_RESOLVER + VALIDATION } enum Platform { @@ -186,19 +150,23 @@ scalar DateTime "The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." scalar Long +scalar UUID + input ArtifactFileInfoFilterInput { and: [ArtifactFileInfoFilterInput!] - downloadSize: ComparableInt64OperationFilterInput - downloads: ComparableInt64OperationFilterInput + downloadSize: LongOperationFilterInput + downloads: LongOperationFilterInput + id: UuidOperationFilterInput md5Hash: StringOperationFilterInput or: [ArtifactFileInfoFilterInput!] } input ArtifactFilterInput { and: [ArtifactFilterInput!] - artifactId: ComparableInt64OperationFilterInput + artifactId: LongOperationFilterInput deltaFileInfo: ArtifactFileInfoFilterInput fileInfo: ArtifactFileInfoFilterInput + id: UuidOperationFilterInput or: [ArtifactFilterInput!] platform: PlatformOperationFilterInput } @@ -208,51 +176,36 @@ input BooleanOperationFilterInput { neq: Boolean } -input ComparableDateTimeOffsetOperationFilterInput { +input DateTimeOperationFilterInput { eq: DateTime gt: DateTime gte: DateTime - in: [DateTime!] + in: [DateTime] lt: DateTime lte: DateTime neq: DateTime ngt: DateTime ngte: DateTime - nin: [DateTime!] + nin: [DateTime] nlt: DateTime nlte: DateTime } -input ComparableInt32OperationFilterInput { +input IntOperationFilterInput { eq: Int gt: Int gte: Int - in: [Int!] + in: [Int] lt: Int lte: Int neq: Int ngt: Int ngte: Int - nin: [Int!] + nin: [Int] nlt: Int nlte: Int } -input ComparableInt64OperationFilterInput { - eq: Long - gt: Long - gte: Long - in: [Long!] - lt: Long - lte: Long - neq: Long - ngt: Long - ngte: Long - nin: [Long!] - nlt: Long - nlte: Long -} - input ListFilterInputTypeOfArtifactFilterInput { all: ArtifactFilterInput any: Boolean @@ -260,6 +213,21 @@ input ListFilterInputTypeOfArtifactFilterInput { some: ArtifactFilterInput } +input LongOperationFilterInput { + eq: Long + gt: Long + gte: Long + in: [Long] + lt: Long + lte: Long + neq: Long + ngt: Long + ngte: Long + nin: [Long] + nlt: Long + nlte: Long +} + input PlatformOperationFilterInput { eq: Platform in: [Platform!] @@ -273,13 +241,13 @@ input ReleaseFilterInput { branch: StringOperationFilterInput changelog: StringOperationFilterInput commit: StringOperationFilterInput - createdAt: ComparableDateTimeOffsetOperationFilterInput - id: StringOperationFilterInput + createdAt: DateTimeOperationFilterInput + id: UuidOperationFilterInput isDraft: BooleanOperationFilterInput or: [ReleaseFilterInput!] - previousRelease: StringOperationFilterInput + previousRelease: ReleaseFilterInput version: StringOperationFilterInput - workflowRunId: ComparableInt64OperationFilterInput + workflowRunId: LongOperationFilterInput } input ReleaseSortInput { @@ -289,27 +257,27 @@ input ReleaseSortInput { createdAt: SortEnumType id: SortEnumType isDraft: SortEnumType - previousRelease: SortEnumType + previousRelease: ReleaseSortInput version: SortEnumType workflowRunId: SortEnumType } input ReleaseStatisticFilterInput { and: [ReleaseStatisticFilterInput!] - count: ComparableInt32OperationFilterInput - lastReportedUsage: ComparableDateTimeOffsetOperationFilterInput - linuxCount: ComparableInt32OperationFilterInput - oSXCount: ComparableInt32OperationFilterInput + count: IntOperationFilterInput + lastReportedUsage: DateTimeOperationFilterInput + linuxCount: IntOperationFilterInput or: [ReleaseStatisticFilterInput!] - releaseId: StringOperationFilterInput - windowsCount: ComparableInt32OperationFilterInput + osxCount: IntOperationFilterInput + releaseId: UuidOperationFilterInput + windowsCount: IntOperationFilterInput } input ReleaseStatisticSortInput { count: SortEnumType lastReportedUsage: SortEnumType linuxCount: SortEnumType - oSXCount: SortEnumType + osxCount: SortEnumType releaseId: SortEnumType windowsCount: SortEnumType } @@ -331,6 +299,21 @@ input StringOperationFilterInput { input UpdateReleaseChangelogInput { changelog: String! - id: String! + id: UUID! isDraft: Boolean! } + +input UuidOperationFilterInput { + eq: UUID + gt: UUID + gte: UUID + in: [UUID] + lt: UUID + lte: UUID + neq: UUID + ngt: UUID + ngte: UUID + nin: [UUID] + nlt: UUID + nlte: UUID +}