1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Implemented most of the updating mechanism

This commit is contained in:
Robert 2023-02-24 22:54:17 +01:00
parent 0cd65a2ebf
commit acd005e4a2
27 changed files with 1055 additions and 383 deletions

View File

@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
void Remove(QueuedActionEntity queuedActionEntity);
List<QueuedActionEntity> GetAll();
List<QueuedActionEntity> GetByType(string type);
bool IsTypeQueued(string type);
void ClearByType(string type);
}

View File

@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
}
/// <inheritdoc />
public bool IsTypeQueued(string type)
{
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
}
/// <inheritdoc />
public void ClearByType(string type)
{
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
}
#endregion
}

View File

@ -0,0 +1,34 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Humanizer;
using Humanizer.Bytes;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts bytes to a string
/// </summary>
public class BytesToStringConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int intBytes)
return intBytes.Bytes().Humanize();
if (value is long longBytes)
return longBytes.Bytes().Humanize();
if (value is double doubleBytes)
return doubleBytes.Bytes().Humanize();
return value;
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string formatted && ByteSize.TryParse(formatted, out ByteSize result))
return result.Bytes;
return value;
}
}

View File

@ -21,6 +21,7 @@
<!-- Custom styles -->
<StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/Skeleton.axaml" />
<StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/Condensed.axaml" />
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />

View File

@ -0,0 +1,174 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Grid ColumnDefinitions="*,*">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="20" Grid.Column="0">
<StackPanel>
<TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock>
<TextBlock Classes="h4">This is heading 4</TextBlock>
<TextBlock Classes="h5">This is heading 5</TextBlock>
<TextBlock Classes="h6">This is heading 6</TextBlock>
<TextBlock>This is regular text</TextBlock>
<TextBlock>This is regular text</TextBlock>
<TextBlock>This is regular text</TextBlock>
</StackPanel>
</Grid>
<Grid Margin="20" Grid.Column="1">
<StackPanel>
<Border Width="400" Classes="skeleton-text h1"></Border>
<Border Width="400" Classes="skeleton-text h2"></Border>
<Border Width="400" Classes="skeleton-text h3"></Border>
<Border Width="400" Classes="skeleton-text h4"></Border>
<Border Width="400" Classes="skeleton-text h5"></Border>
<Border Width="400" Classes="skeleton-text h6"></Border>
<Border Width="400" Classes="skeleton-text"></Border>
<Border Width="400" Classes="skeleton-text"></Border>
<Border Width="400" Classes="skeleton-text"></Border>
</StackPanel>
<StackPanel>
<StackPanel.Styles>
<Style Selector="TextBlock">
<Setter Property="Background" Value="#55ff0000"></Setter>
</Style>
</StackPanel.Styles>
<TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock>
<TextBlock Classes="h4">This is heading 4</TextBlock>
<TextBlock Classes="h5">This is heading 5</TextBlock>
<TextBlock Classes="h6">This is heading 6</TextBlock>
<TextBlock>This is regular text</TextBlock>
</StackPanel>
</Grid>
<Grid Margin="20" Grid.Column="0" Row="1">
<StackPanel Spacing="2">
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h4 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h5 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h6 no-margin"></Border>
<Border Width="400" Classes="skeleton-text no-margin"></Border>
<Border Width="400" Classes="skeleton-text no-margin"></Border>
<Border Width="400" Classes="skeleton-text no-margin"></Border>
</StackPanel>
<StackPanel Spacing="2">
<StackPanel.Styles>
<Style Selector="TextBlock">
<Setter Property="Background" Value="#55ff0000"></Setter>
</Style>
</StackPanel.Styles>
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
<TextBlock Classes="h4 no-margin">This is heading 4</TextBlock>
<TextBlock Classes="h5 no-margin">This is heading 5</TextBlock>
<TextBlock Classes="h6 no-margin">This is heading 6</TextBlock>
<TextBlock>This is regular text</TextBlock>
</StackPanel>
</Grid>
</Grid>
</Design.PreviewWith>
<Styles.Resources>
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
</Styles.Resources>
<Style Selector="Border.skeleton-text">
<Setter Property="Height" Value="17"></Setter>
<Setter Property="Margin" Value="0 1" />
<Setter Property="IsHitTestVisible" Value="False" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Style.Animations>
<Animation Duration="0:0:1.5" IterationCount="Infinite" PlaybackDirection="Normal">
<KeyFrame Cue="0%">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="-100%,-100%" EndPoint="0%,0%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Gray" />
<GradientStop Offset="0.4" Color="#595959" />
<GradientStop Offset="0.6" Color="#595959" />
<GradientStop Offset="1" Color="Gray" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="100%,100%" EndPoint="200%,200%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Gray" />
<GradientStop Offset="0.4" Color="#595959" />
<GradientStop Offset="0.6" Color="#595959" />
<GradientStop Offset="1" Color="Gray" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.skeleton-text.h1">
<Setter Property="Height" Value="65" />
<Setter Property="Margin" Value="0 10 0 20" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Border.skeleton-text.h2">
<Setter Property="Height" Value="44" />
<Setter Property="Margin" Value="0 10 0 20" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Border.skeleton-text.h3">
<Setter Property="Height" Value="33" />
<Setter Property="Margin" Value="0 5 0 15" />
</Style>
<Style Selector="Border.skeleton-text.h4">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="0 2 0 12" />
</Style>
<Style Selector="Border.skeleton-text.h5">
<Setter Property="Height" Value="20" />
<Setter Property="Margin" Value="0 2 0 7" />
</Style>
<Style Selector="Border.skeleton-text.h6">
<Setter Property="Height" Value="15" />
<Setter Property="Margin" Value="0 2 0 4" />
</Style>
<Style Selector="Border.skeleton-text.h1.no-margin">
<Setter Property="Margin" Value="0 10 0 10" />
</Style>
<Style Selector="Border.skeleton-text.h2.no-margin">
<Setter Property="Margin" Value="0 10 0 10" />
</Style>
<Style Selector="Border.skeleton-text.h3.no-margin">
<Setter Property="Margin" Value="0 5 0 5" />
</Style>
<Style Selector="Border.skeleton-text.h4.no-margin">
<Setter Property="Margin" Value="0 2 0 2" />
</Style>
<Style Selector="Border.skeleton-text.h5.no-margin">
<Setter Property="Margin" Value="0 2 0 2" />
</Style>
<Style Selector="Border.skeleton-text.h6.no-margin">
<Setter Property="Margin" Value="0 2 0 2" />
</Style>
</Styles>

View File

@ -108,8 +108,6 @@ public class ApplicationStateManager
ProcessStartInfo info = new()
{
Arguments = $"-File {script} {source} {destination} {args}",
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true,
FileName = "PowerShell.exe"
};
Process.Start(info);

View File

@ -4,6 +4,10 @@ param (
[Parameter(Mandatory=$false)][string]$artemisArgs
)
Write-Host "Artemis update script v1"
Write-Host "Please do not close this window, this should not take long"
Write-Host ""
# Wait up to 10 seconds for the process to shut down
for ($i=1; $i -le 10; $i++) {
$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue
@ -26,12 +30,17 @@ if (!(Test-Path $destinationDirectory)) {
Write-Error "The destination directory does not exist"
}
# If the destination directory exists, clear it
Write-Host "Cleaning up old version where needed"
Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force
# Move the contents of the source directory to the destination directory
Write-Host "Installing new files"
Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory
Write-Host "Finished! Restarting Artemis"
Start-Sleep -Seconds 1
# When finished, run the updated version

View File

@ -43,4 +43,15 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Compile Update="Screens\Settings\Tabs\ReleasesTabView.axaml.cs">
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Settings\Updating\ReleaseView.axaml.cs">
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -31,14 +31,13 @@ public static class UIContainerExtensions
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<MainScreenViewModel>(), ifAlreadyRegistered: IfAlreadyRegistered.Replace);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
container.Register<IPropertyVmFactory, PropertyVmFactory>(Reuse.Singleton);
container.Register<IUpdateNotificationProvider, SimpleUpdateNotificationProvider>();
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IArtemisUIService>(), Reuse.Singleton);
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System;
using System.Collections.ObjectModel;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting;
@ -474,4 +476,23 @@ public class ScriptVmFactory : IScriptVmFactory
{
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
}
}
public interface IReleaseVmFactory : IVmFactory
{
ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt);
}
public class ReleaseVmFactory : IReleaseVmFactory
{
private readonly IContainer _container;
public ReleaseVmFactory(IContainer container)
{
_container = container;
}
public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt)
{
return _container.Resolve<ReleaseViewModel>(new object[] { releaseId, version, createdAt });
}
}

View File

@ -10,6 +10,7 @@ public class SettingsViewModel : MainScreenViewModel
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
{
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
@ -17,6 +18,7 @@ public class SettingsViewModel : MainScreenViewModel
generalTabViewModel,
pluginsTabViewModel,
devicesTabViewModel,
releasesTabViewModel,
aboutTabViewModel
};
}

View File

@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
x:DataType="settings:ReleasesTabViewModel">
<Grid ColumnDefinitions="300,*" Margin="0 10">
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
<ListBox Items="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="updating:ReleaseViewModel">
<StackPanel Margin="4">
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}"/>
</Grid>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings;
public class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
{
public ReleasesTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings;
public class ReleasesTabViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService;
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private bool _loading;
private ReleaseViewModel? _selectedReleaseViewModel;
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_releases = new SourceList<IGetReleases_PublishedReleases_Nodes>();
_releases.Connect()
.Sort(SortExpressionComparer<IGetReleases_PublishedReleases_Nodes>.Descending(p => p.CreatedAt))
.Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt))
.ObserveOn(AvaloniaScheduler.Instance)
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
.Subscribe();
DisplayName = "Releases";
ReleaseViewModels = releaseViewModels;
this.WhenActivated(async d =>
{
await updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault();
});
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public ReleaseViewModel? SelectedReleaseViewModel
{
get => _selectedReleaseViewModel;
set => RaiseAndSetIfChanged(ref _selectedReleaseViewModel, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public async Task GetMoreReleases(CancellationToken cancellationToken)
{
if (_lastPageInfo != null && !_lastPageInfo.HasNextPage)
return;
try
{
Loading = true;
IOperationResult<IGetReleasesResult> result = await _updatingClient.GetReleases.ExecuteAsync("feature/gh-actions", Platform.Windows, 20, _lastPageInfo?.EndCursor, cancellationToken);
if (result.Data?.PublishedReleases?.Nodes == null)
return;
_lastPageInfo = result.Data.PublishedReleases.PageInfo;
_releases.AddRange(result.Data.PublishedReleases.Nodes);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve releases");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve releases")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
}

View File

@ -1,29 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseAvailableView : ReactiveCoreWindow<ReleaseAvailableViewModel>
{
public ReleaseAvailableView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
Close();
}
}

View File

@ -1,76 +0,0 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseAvailableViewModel : ActivatableViewModelBase
{
private readonly string _nextReleaseId;
private readonly ILogger _logger;
private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService;
private IGetReleaseById_Release? _release;
public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService)
{
_nextReleaseId = nextReleaseId;
_logger = logger;
_updateService = updateService;
_updatingClient = updatingClient;
_notificationService = notificationService;
CurrentVersion = _updateService.CurrentVersion ?? "Development build";
Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null));
this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken()));
}
private void ExecuteInstall()
{
_updateService.InstallRelease(_nextReleaseId);
}
private async Task RetrieveRelease(CancellationToken cancellationToken)
{
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken);
// Borrow GraphQLClientException for messaging, how lazy of me..
if (result.Errors.Count > 0)
{
GraphQLClientException exception = new(result.Errors);
_logger.Error(exception, "Failed to retrieve release details");
_notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show();
return;
}
if (result.Data?.Release == null)
{
_notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show();
return;
}
Release = result.Data.Release;
}
public string CurrentVersion { get; }
public IGetReleaseById_Release? Release
{
get => _release;
set => RaiseAndSetIfChanged(ref _release, value);
}
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> AskLater { get; }
}

View File

@ -1,40 +0,0 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
mc:Ignorable="d"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseInstallerView"
x:DataType="updating:ReleaseInstallerViewModel"
Title="Artemis | Updating"
ShowAsDialog="True"
Width="465" Height="260"
Padding="15"
CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,*,Auto" Width="450" Height="200">
<TextBlock Grid.Row="0" Classes="h4" TextWrapping="Wrap">
Downloading &amp; installing update...
</TextBlock>
<TextBlock Grid.Row="1" Classes="subtitle" TextWrapping="Wrap">
This should not take long, when finished Artemis must restart.
</TextBlock>
<StackPanel Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Stretch" IsVisible="{CompiledBinding !Ready}">
<ProgressBar Value="{CompiledBinding OverallProgress}" ></ProgressBar>
<ProgressBar Margin="0 15 0 0" Value="{CompiledBinding StepProgress}"></ProgressBar>
</StackPanel>
<TextBlock Grid.Row="2" IsVisible="{CompiledBinding Ready}" VerticalAlignment="Top" Margin="0 15 0 0">Done, click restart to apply the update 🫡</TextBlock>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="15" Grid.Row="3" Margin="0 15 0 0" Height="30">
<CheckBox IsVisible="{CompiledBinding !Ready}" IsChecked="{CompiledBinding RestartWhenFinished}">Restart when finished</CheckBox>
<Button IsVisible="{CompiledBinding Ready}" Command="{CompiledBinding Restart}" Classes="accent">Restart</Button>
<Button Click="Cancel_OnClick">Cancel</Button>
</StackPanel>
</Grid>
</controls:CoreWindow>

View File

@ -1,28 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseInstallerView : ReactiveCoreWindow<ReleaseInstallerViewModel>
{
public ReleaseInstallerView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Cancel_OnClick(object? sender, RoutedEventArgs e)
{
Close();
}
}

View File

@ -1,81 +0,0 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseInstallerViewModel : ActivatableViewModelBase
{
private readonly ReleaseInstaller _releaseInstaller;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<float>? _overallProgress;
private ObservableAsPropertyHelper<float>? _stepProgress;
private bool _ready;
private bool _restartWhenFinished;
public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService)
{
_releaseInstaller = releaseInstaller;
_windowService = windowService;
Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
this.WhenActivated(d =>
{
_overallProgress = Observable.FromEventPattern<float>(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x)
.Select(e => e.EventArgs)
.ToProperty(this, vm => vm.OverallProgress)
.DisposeWith(d);
_stepProgress = Observable.FromEventPattern<float>(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x)
.Select(e => e.EventArgs)
.ToProperty(this, vm => vm.StepProgress)
.DisposeWith(d);
Task.Run(() => InstallUpdate(d.AsCancellationToken()));
});
}
public ReactiveCommand<Unit, Unit> Restart { get; }
public float OverallProgress => _overallProgress?.Value ?? 0;
public float StepProgress => _stepProgress?.Value ?? 0;
public bool Ready
{
get => _ready;
set => RaiseAndSetIfChanged(ref _ready, value);
}
public bool RestartWhenFinished
{
get => _restartWhenFinished;
set => RaiseAndSetIfChanged(ref _restartWhenFinished, value);
}
private async Task InstallUpdate(CancellationToken cancellationToken)
{
try
{
await _releaseInstaller.InstallAsync(cancellationToken);
Ready = true;
if (RestartWhenFinished)
Utilities.ApplyUpdate(false);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Something went wrong while installing the update", e);
}
}
}

View File

@ -1,49 +1,163 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseAvailableView"
x:DataType="updating:ReleaseAvailableViewModel"
Title="Artemis | Update available"
Width="750"
Height="750"
WindowStartupLocation="CenterOwner">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
x:DataType="updating:ReleaseViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector=":is(Control).fade-in">
<Setter Property="Opacity" Value="0"></Setter>
</Style>
<Style Selector=":is(Control).fade-in[IsVisible=True]">
<Style.Animations>
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Grid.info-container">
<Setter Property="Margin" Value="10" />
</Style>
<Style Selector="avalonia1|MaterialIcon.info-icon">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="0 3 10 0" />
</Style>
<Style Selector="TextBlock.info-title">
<Setter Property="Margin" Value="0 0 0 5" />
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="TextBlock.info-body">
</Style>
<Style Selector="TextBlock.info-link">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
</Style>
<Style Selector="TextBlock.info-link:pointerover">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
</Style>
</UserControl.Styles>
<TextBlock Grid.Row="0" Classes="h4">
A new Artemis update is available! 🥳
</TextBlock>
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding !Loading}" Classes="fade-in">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock>Retrieving release...</TextBlock>
<ProgressBar IsIndeterminate="True"></ProgressBar>
</StackPanel>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
<Grid ColumnDefinitions="*,*"
RowDefinitions="*,*"
IsVisible="{CompiledBinding InstallationInProgress}">
<ProgressBar Grid.Column="0"
Grid.Row="0"
Width="300"
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
</ProgressBar>
<TextBlock Grid.Column="0"
Grid.Row="1"
Classes="subtitle"
TextAlignment="Right"
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
Classes="accent"
Margin="15 0 0 0"
Width="80"
VerticalAlignment="Center"
Command="{CompiledBinding CancelInstall}">
Cancel
</Button>
</Grid>
<Border Grid.Row="1" Classes="card" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="You are currently running version " />
<TextBlock Text="{CompiledBinding CurrentVersion, Mode=OneWay}"></TextBlock>
<TextBlock Text=" while the latest build is " />
<TextBlock Text="{CompiledBinding Release.Version, Mode=OneWay, FallbackValue='Unknown'}"></TextBlock>
<TextBlock Text="." />
</StackPanel>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<TextBlock Text="Updating Artemis will give you the latest bug(fixes), features and improvements." />
<Separator Classes="card-separator" />
</StackPanel>
<!-- Restart button -->
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
<TextBlock Grid.Column="0"
Grid.Row="0"
Classes="subtitle"
TextAlignment="Right"
VerticalAlignment="Center">
Ready, restart to install
</TextBlock>
<Button Grid.Column="1" Grid.Row="0"
Classes="accent"
Margin="15 0 0 0"
Width="80"
Command="{CompiledBinding Restart}"
IsVisible="{CompiledBinding InstallationFinished}">
Restart
</Button>
</Grid>
</Panel>
</Panel>
<avalonia:MarkdownScrollViewer Grid.Row="1"
VerticalAlignment="Top"
Markdown="{CompiledBinding Release.Changelog}">
</Grid>
<Separator Classes="card-separator" />
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
<avalonia1:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body"
Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body info-link"
Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased"
Text="{CompiledBinding ShortCommit, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
<avalonia1:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body"
Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay, FallbackValue=Loading...}" />
</Grid>
</Grid>
</StackPanel>
</Border>
<Border Grid.Row="1" Classes="card">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
<Separator Grid.Row="1" Classes="card-separator" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Changelog}">
<avalonia:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Style.Setters>
@ -207,10 +321,6 @@
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Grid.Row="2" Margin="0 15 0 0">
<Button Classes="accent" Command="{CompiledBinding Install}" Click="Button_OnClick">Install update</Button>
<Button Click="Button_OnClick">Ask later</Button>
</StackPanel>
</Grid>
</controls:CoreWindow>
</UserControl>

View File

@ -0,0 +1,23 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseView : ReactiveUserControl<ReleaseViewModel>
{
public ReleaseView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.NavigateToSource();
}
}

View File

@ -0,0 +1,202 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly string _releaseId;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly IWindowService _windowService;
private CancellationTokenSource? _installerCts;
private string _changelog = string.Empty;
private string _commit = string.Empty;
private string _shortCommit = string.Empty;
private long _fileSize;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private bool _loading = true;
private bool _retrievedDetails;
public ReleaseViewModel(string releaseId,
string version,
DateTimeOffset createdAt,
ILogger logger,
IUpdatingClient updatingClient,
INotificationService notificationService,
IUpdateService updateService,
IWindowService windowService)
{
_releaseId = releaseId;
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_windowService = windowService;
if (OperatingSystem.IsWindows())
_updatePlatform = Platform.Windows;
else if (OperatingSystem.IsLinux())
_updatePlatform = Platform.Linux;
else if (OperatingSystem.IsMacOS())
_updatePlatform = Platform.Osx;
else
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
Version = version;
CreatedAt = createdAt;
ReleaseInstaller = updateService.GetReleaseInstaller(_releaseId);
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
this.WhenActivated(d =>
{
// There's no point in running anything but the latest version of the current channel.
// Perhaps later that won't be true anymore, then we could consider allowing to install
// older versions with compatible database versions.
InstallationAvailable = updateService.CachedLatestRelease?.Id == _releaseId;
RetrieveDetails(d.AsCancellationToken()).ToObservable();
Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d);
});
}
public ReactiveCommand<Unit, Unit> Restart { get; set; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
public string Version { get; }
public DateTimeOffset CreatedAt { get; }
public ReleaseInstaller ReleaseInstaller { get; }
public string Changelog
{
get => _changelog;
set => RaiseAndSetIfChanged(ref _changelog, value);
}
public string Commit
{
get => _commit;
set => RaiseAndSetIfChanged(ref _commit, value);
}
public string ShortCommit
{
get => _shortCommit;
set => RaiseAndSetIfChanged(ref _shortCommit, value);
}
public long FileSize
{
get => _fileSize;
set => RaiseAndSetIfChanged(ref _fileSize, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public bool InstallationAvailable
{
get => _installationAvailable;
set => RaiseAndSetIfChanged(ref _installationAvailable, value);
}
public bool InstallationInProgress
{
get => _installationInProgress;
set => RaiseAndSetIfChanged(ref _installationInProgress, value);
}
public bool InstallationFinished
{
get => _installationFinished;
set => RaiseAndSetIfChanged(ref _installationFinished, value);
}
public void NavigateToSource()
{
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}");
}
private async Task ExecuteInstall(CancellationToken cancellationToken)
{
_installerCts = new CancellationTokenSource();
try
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
InstallationFinished = true;
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Failed to install update", e);
}
finally
{
InstallationInProgress = false;
}
}
private async Task RetrieveDetails(CancellationToken cancellationToken)
{
if (_retrievedDetails)
return;
try
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;
Changelog = release.Changelog;
Commit = release.Commit;
ShortCommit = release.Commit.Substring(0, 7);
FileSize = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)?.FileInfo.DownloadSize ?? 0;
_retrievedDetails = true;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve release details");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve details")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
}

View File

@ -1,11 +1,18 @@
using System.Threading.Tasks;
using Artemis.UI.Services.Interfaces;
using Artemis.WebClient.Updating;
namespace Artemis.UI.Services.Updating;
public interface IUpdateService : IArtemisUIService
{
string? CurrentVersion { get; }
IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; }
Task CacheLatestRelease();
Task<bool> CheckForUpdate();
Task InstallRelease(string releaseId);
string? CurrentVersion { get; }
void QueueUpdate();
ReleaseInstaller GetReleaseInstaller(string releaseId);
}

View File

@ -3,9 +3,11 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Artemis.UI.Extensions;
using Artemis.WebClient.Updating;
using Octodiff.Core;
@ -18,7 +20,7 @@ namespace Artemis.UI.Services.Updating;
/// <summary>
/// Represents the installation process of a release
/// </summary>
public class ReleaseInstaller
public class ReleaseInstaller : CorePropertyChanged
{
private readonly string _dataFolder;
private readonly HttpClient _httpClient;
@ -26,6 +28,10 @@ public class ReleaseInstaller
private readonly string _releaseId;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly Progress<float> _progress = new();
private Progress<float> _stepProgress = new();
private string _status;
private float _progress1;
public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient)
{
@ -46,29 +52,42 @@ public class ReleaseInstaller
if (!Directory.Exists(_dataFolder))
Directory.CreateDirectory(_dataFolder);
_progress.ProgressChanged += (_, f) => Progress = f;
}
public string Status
{
get => _status;
private set => SetAndNotify(ref _status, value);
}
public Progress<float> OverallProgress { get; } = new();
public Progress<float> StepProgress { get; } = new();
public float Progress
{
get => _progress1;
set => SetAndNotify(ref _progress1, value);
}
public async Task InstallAsync(CancellationToken cancellationToken)
{
((IProgress<float>) OverallProgress).Report(0);
_stepProgress = new Progress<float>();
((IProgress<float>) _progress).Report(0);
Status = "Retrieving details";
_logger.Information("Retrieving details for release {ReleaseId}", _releaseId);
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
result.EnsureNoErrors();
IGetReleaseById_Release? release = result.Data?.Release;
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
throw new Exception($"Could not find release with ID {_releaseId}");
IGetReleaseById_Release_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform);
IGetReleaseById_PublishedRelease_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");
((IProgress<float>) OverallProgress).Report(10);
((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)
@ -77,75 +96,92 @@ public class ReleaseInstaller
await Download(artifact, cancellationToken);
}
private async Task DownloadDelta(IGetReleaseById_Release_Artifacts artifact, string previousRelease, CancellationToken cancellationToken)
private async Task DownloadDelta(IGetReleaseById_PublishedRelease_Artifacts artifact, string previousRelease, CancellationToken cancellationToken)
{
// 10 - 50%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.4f);
Status = "Downloading...";
await using MemoryStream stream = new();
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken);
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken);
((IProgress<float>) OverallProgress).Report(33);
await PatchDelta(stream, previousRelease, cancellationToken);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
await PatchDelta(stream, previousRelease, artifact, cancellationToken);
}
private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken)
private async Task PatchDelta(Stream deltaStream, string previousRelease, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
{
await using FileStream baseStream = File.OpenRead(previousRelease);
// 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);
deltaStream.Seek(0, SeekOrigin.Begin);
await Task.Run(() =>
await using (FileStream baseStream = File.OpenRead(previousRelease))
{
DeltaApplier deltaApplier = new();
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream);
});
cancellationToken.ThrowIfCancellationRequested();
deltaStream.Seek(0, SeekOrigin.Begin);
DeltaApplier deltaApplier = new() {SkipHashCheck = true};
// Patching is not async and so fast that it's not worth adding a progress reporter
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new NullProgressReporter()), newFileStream);
cancellationToken.ThrowIfCancellationRequested();
}
((IProgress<float>) OverallProgress).Report(66);
// The previous release is no longer required now that the latest has been downloaded
File.Delete(previousRelease);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
await ValidateArchive(newFileStream, artifact, cancellationToken);
await Extract(newFileStream, cancellationToken);
}
private async Task Download(IGetReleaseById_Release_Artifacts artifact, CancellationToken cancellationToken)
private async Task Download(IGetReleaseById_PublishedRelease_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);
// 10 - 60%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.5f);
((IProgress<float>) OverallProgress).Report(50);
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);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
await ValidateArchive(stream, artifact, cancellationToken);
await Extract(stream, cancellationToken);
}
private async Task Extract(Stream archiveStream, CancellationToken cancellationToken)
{
// 60 - 100%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(60f + e * 0.4f);
Status = "Extracting...";
// Ensure the directory is empty
string extractDirectory = Path.Combine(_dataFolder, "pending");
if (Directory.Exists(extractDirectory))
Directory.Delete(extractDirectory, true);
Directory.CreateDirectory(extractDirectory);
await Task.Run(() =>
{
archiveStream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(archiveStream);
archive.ExtractToDirectory(extractDirectory, false, StepProgress, cancellationToken);
});
((IProgress<float>) OverallProgress).Report(100);
}
}
archive.ExtractToDirectory(extractDirectory, false, _stepProgress, cancellationToken);
}, cancellationToken);
internal class DeltaApplierProgressReporter : IProgressReporter
{
private readonly IProgress<float> _stepProgress;
public DeltaApplierProgressReporter(IProgress<float> stepProgress)
{
_stepProgress = stepProgress;
((IProgress<float>) _progress).Report(100);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
}
public void ReportProgress(string operation, long currentPosition, long total)
private async Task ValidateArchive(Stream archiveStream, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
{
_stepProgress.Report(currentPosition / total * 100);
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}\"");
}
}

View File

@ -4,8 +4,9 @@ using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
using Artemis.WebClient.Updating;
@ -14,7 +15,7 @@ using Serilog;
using StrawberryShake;
using Timer = System.Timers.Timer;
namespace Artemis.UI.Services;
namespace Artemis.UI.Services.Updating;
public class UpdateService : IUpdateService
{
@ -26,24 +27,24 @@ public class UpdateService : IUpdateService
private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService;
private readonly IQueuedActionRepository _queuedActionRepository;
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly IWindowService _windowService;
private bool _suspendAutoCheck;
public UpdateService(ILogger logger,
ISettingsService settingsService,
IMainWindowService mainWindowService,
IWindowService windowService,
IQueuedActionRepository queuedActionRepository,
IUpdatingClient updatingClient,
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
Func<string, ReleaseInstaller> getReleaseInstaller)
{
_logger = logger;
_mainWindowService = mainWindowService;
_windowService = windowService;
_queuedActionRepository = queuedActionRepository;
_updatingClient = updatingClient;
_updateNotificationProvider = updateNotificationProvider;
_getReleaseInstaller = getReleaseInstaller;
@ -65,24 +66,32 @@ public class UpdateService : IUpdateService
Timer timer = new(UPDATE_CHECK_INTERVAL);
timer.Elapsed += HandleAutoUpdateEvent;
timer.Start();
_channel.Value = "feature/gh-actions";
_channel.Save();
InstallQueuedUpdate();
}
public string? CurrentVersion
private void InstallQueuedUpdate()
{
get
{
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
}
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
return;
_queuedActionRepository.ClearByType("InstallUpdate");
_logger.Information("Installing queued update");
Utilities.ApplyUpdate(false);
}
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<ReleaseAvailableViewModel>(nextReleaseId);
});
}
@ -112,19 +121,35 @@ public class UpdateService : IUpdateService
_logger.Warning(ex, "Auto update failed");
}
}
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
public string? CurrentVersion
{
get
{
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
}
}
/// <inheritdoc />
public async Task CacheLatestRelease()
{
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
CachedLatestRelease = result.Data?.NextPublishedRelease;
}
public async Task<bool> CheckForUpdate()
{
string? currentVersion = CurrentVersion;
if (currentVersion == null)
return false;
// IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform);
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, "feature/gh-actions", _updatePlatform);
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
result.EnsureNoErrors();
// Update cache
CachedLatestRelease = result.Data?.NextPublishedRelease;
// No update was found
if (result.Data?.NextRelease == null)
if (CachedLatestRelease == null)
return false;
// Only offer it once per session
@ -132,15 +157,15 @@ public class UpdateService : IUpdateService
// 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);
await ShowUpdateDialog(CachedLatestRelease.Id);
else if (!_autoInstall.Value)
await ShowUpdateNotification(result.Data.NextRelease.Id);
await ShowUpdateNotification(CachedLatestRelease.Id);
else
await AutoInstallUpdate(result.Data.NextRelease.Id);
await AutoInstallUpdate(CachedLatestRelease.Id);
return true;
}
/// <inheritdoc />
public async Task InstallRelease(string releaseId)
{
@ -149,7 +174,19 @@ public class UpdateService : IUpdateService
{
// Main window is probably already open but this will bring it into focus
_mainWindowService.OpenMainWindow();
_windowService.ShowWindow<ReleaseInstallerViewModel>(installer);
});
}
/// <inheritdoc />
public void QueueUpdate()
{
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
_queuedActionRepository.Add(new QueuedActionEntity {Type = "InstallUpdate"});
}
/// <inheritdoc />
public ReleaseInstaller GetReleaseInstaller(string releaseId)
{
return _getReleaseInstaller(releaseId);
}
}

View File

@ -1,5 +1,29 @@
query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: String) {
publishedReleases(
first: $take
after: $after
where: {
and: [
{ branch: { eq: $branch } }
{ artifacts: { some: { platform: { eq: $platform } } } }
]
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
version
createdAt
}
}
}
query GetReleaseById($id: String!) {
release(id: $id) {
publishedRelease(id: $id) {
branch
commit
version
@ -23,9 +47,10 @@ fragment fileInfo on ArtifactFileInfo {
downloadSize
}
query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) {
nextRelease(version: $currentVersion, branch: $branch, platform: $platform) {
query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) {
nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) {
id
version
}
}
}

View File

@ -41,7 +41,6 @@ type Artifact {
artifactId: Long!
deltaFileInfo: ArtifactFileInfo!
fileInfo: ArtifactFileInfo!
fileName(deltaFile: Boolean!): String!
platform: Platform!
}
@ -55,13 +54,69 @@ type Mutation {
updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload!
}
"Information about pagination in a connection."
type PageInfo {
"When paginating forwards, the cursor to continue."
endCursor: String
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
}
"A connection to a list of items."
type PublishedReleasesConnection {
"A list of edges."
edges: [PublishedReleasesEdge!]
"A flattened list of the nodes."
nodes: [Release!]
"Information to aid in pagination."
pageInfo: PageInfo!
totalCount: Int!
}
"An edge in a connection."
type PublishedReleasesEdge {
"A cursor for use in pagination."
cursor: String!
"The item at the end of the edge."
node: Release!
}
type Query {
channelByBranch(branch: String!): ArtemisChannel
channels: [ArtemisChannel!]!
nextRelease(branch: String!, platform: Platform!, version: String!): Release
nextPublishedRelease(branch: String!, platform: Platform!, version: String): Release
publishedChannels: [String!]!
publishedRelease(id: String!): Release
publishedReleases(
"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
): PublishedReleasesConnection
release(id: String!): Release
releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]!
releases(order: [ReleaseSortInput!], where: ReleaseFilterInput): [Release!]!
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
}
type Release {
@ -86,6 +141,25 @@ type ReleaseStatistic {
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!]
"Information to aid in pagination."
pageInfo: PageInfo!
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
}