1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Simplify release install process

Fix install on startup
This commit is contained in:
Robert 2023-03-06 21:58:44 +01:00
parent 09a2f769c3
commit 4a8845e578
18 changed files with 237 additions and 253 deletions

View File

@ -48,6 +48,10 @@ public static class Constants
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary>
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string UpdatingFolder = Path.Combine(DataFolder, "updating");
/// <summary>
/// The full path to the Artemis plugins folder

View File

@ -21,6 +21,7 @@ public static class Utilities
CreateAccessibleDirectory(Constants.DataFolder);
CreateAccessibleDirectory(Constants.PluginsFolder);
CreateAccessibleDirectory(Constants.LayoutsFolder);
CreateAccessibleDirectory(Constants.UpdatingFolder);
}
/// <summary>

View File

@ -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
}

View File

@ -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<ReleaseEntity>().EnsureIndex(s => s.Version, true);
_repository.Database.GetCollection<ReleaseEntity>().EnsureIndex(s => s.Status);
}
public ReleaseEntity GetQueuedVersion()
public void SaveVersionInstallDate(string version)
{
return _repository.Query<ReleaseEntity>().Where(r => r.Status == ReleaseEntityStatus.Queued).FirstOrDefault();
}
ReleaseEntity release = _repository.Query<ReleaseEntity>().Where(r => r.Version == version).FirstOrDefault();
if (release != null)
return;
public ReleaseEntity GetInstalledVersion()
{
return _repository.Query<ReleaseEntity>().Where(r => r.Status == ReleaseEntityStatus.Installed).FirstOrDefault();
_repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow});
}
public ReleaseEntity GetPreviousInstalledVersion()
{
return _repository.Query<ReleaseEntity>().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<ReleaseEntity>().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<ReleaseEntity>().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<ReleaseEntity> oldReleases = _repository.Query<ReleaseEntity>().Where(r => r.Version != version && r.Status != ReleaseEntityStatus.Historical).ToList();
foreach (ReleaseEntity oldRelease in oldReleases)
oldRelease.Status = ReleaseEntityStatus.Historical;
_repository.Update<ReleaseEntity>(oldReleases);
}
public void DequeueInstallation()
{
// Mark all queued releases as unknown, until FinishInstallation is called we don't know the status
List<ReleaseEntity> queuedReleases = _repository.Query<ReleaseEntity>().Where(r => r.Status == ReleaseEntityStatus.Queued).ToList();
foreach (ReleaseEntity queuedRelease in queuedReleases)
queuedRelease.Status = ReleaseEntityStatus.Unknown;
_repository.Update<ReleaseEntity>(queuedReleases);
return _repository.Query<ReleaseEntity>().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();
}

View File

@ -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!);
}

View File

@ -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)}\"" : "";

View File

@ -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<string, ReleaseInstaller> _getReleaseInstaller;
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService;
private readonly IUpdateService _updateService;
@ -25,7 +26,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
IUpdateService updateService,
Func<IScreen, SettingsViewModel> getSettingsViewModel,
Func<string, ReleaseInstaller> getReleaseInstaller)
Func<Guid, ReleaseInstaller> 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)

View File

@ -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<ReleaseViewModel>(new object[] { releaseId, version, createdAt });
}

View File

@ -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();

View File

@ -61,7 +61,7 @@ public class ReleasesTabViewModel : ActivatableViewModelBase
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public string Channel { get; }
public string? PreselectId { get; set; }
public Guid? PreselectId { get; set; }
public ReleaseViewModel? SelectedReleaseViewModel
{

View File

@ -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)

View File

@ -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);
}

View File

@ -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
{
/// <summary>
/// Gets the current update channel.
/// </summary>
string Channel { get; }
/// <summary>
/// Gets the version number of the previous release that was installed, if any.
/// </summary>
string? PreviousVersion { get; }
/// <summary>
/// The latest cached release, can be updated by calling <see cref="CachedLatestRelease" />.
/// </summary>
IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; }
/// <summary>
/// Asynchronously caches the latest release.
/// </summary>
Task CacheLatestRelease();
Task<bool> CheckForUpdate();
void QueueUpdate(string version, string releaseId);
ReleaseInstaller GetReleaseInstaller(string releaseId);
/// <summary>
/// Asynchronously checks whether an update is available on the current <see cref="Channel" />.
/// </summary>
Task<bool> CheckForUpdate();
/// <summary>
/// Creates a release installed for a release with the provided ID.
/// </summary>
/// <param name="releaseId">The ID of the release to create the installer for.</param>
/// <returns>The resulting release installer.</returns>
ReleaseInstaller GetReleaseInstaller(Guid releaseId);
/// <summary>
/// Restarts the application to install a pending update.
/// </summary>
/// <param name="silent">A boolean indicating whether to perform a silent install of the update.</param>
void RestartForUpdate(bool silent);
/// <summary>
/// Initializes the update service.
/// </summary>
/// <returns>A boolean indicating whether a restart will occur to install a pending update.</returns>
bool Initialize();
}

View File

@ -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
}
/// <inheritdoc />
public void ShowNotification(string releaseId, string releaseVersion)
public void ShowNotification(Guid releaseId, string releaseVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowInAppNotification(releaseId, releaseVersion);

View File

@ -22,24 +22,26 @@ namespace Artemis.UI.Services.Updating;
/// </summary>
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<float> _progress = new();
private IGetReleaseById_PublishedRelease _release = null!;
private IGetReleaseById_PublishedRelease_Artifacts _artifact = null!;
private Progress<float> _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<IGetReleaseByIdResult> 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<float>) _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<float>) _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<float>) _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}\"");
}
}

View File

@ -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<bool> _autoCheck;
private readonly PluginSetting<bool> _autoInstall;
private readonly Func<string, ReleaseInstaller> _getReleaseInstaller;
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
private readonly ILogger _logger;
private readonly IReleaseRepository _releaseRepository;
@ -36,7 +37,7 @@ public class UpdateService : IUpdateService
IUpdatingClient updatingClient,
IReleaseRepository releaseRepository,
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
Func<string, ReleaseInstaller> getReleaseInstaller)
Func<Guid, ReleaseInstaller> 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
}
}
/// <inheritdoc />
public string Channel { get; }
public string? PreviousVersion { get; set; }
/// <inheritdoc />
public string? PreviousVersion { get; private set; }
/// <inheritdoc />
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
/// <inheritdoc />
@ -158,6 +146,7 @@ public class UpdateService : IUpdateService
}
}
/// <inheritdoc />
public async Task<bool> CheckForUpdate()
{
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform);
@ -183,13 +172,7 @@ public class UpdateService : IUpdateService
}
/// <inheritdoc />
public void QueueUpdate(string version, string releaseId)
{
_releaseRepository.QueueInstallation(version, releaseId);
}
/// <inheritdoc />
public ReleaseInstaller GetReleaseInstaller(string releaseId)
public ReleaseInstaller GetReleaseInstaller(Guid releaseId)
{
return _getReleaseInstaller(releaseId);
}
@ -197,7 +180,33 @@ public class UpdateService : IUpdateService
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
}

View File

@ -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

View File

@ -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
}