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

Workshop - Auto-updating WIP

This commit is contained in:
Robert 2024-07-14 22:14:11 +02:00
parent 99d11e1921
commit 3b2d799bfc
18 changed files with 379 additions and 108 deletions

View File

@ -1,17 +1,15 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.UI.Notifications; using Windows.UI.Notifications;
using Artemis.UI.Screens.Settings; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading; using Avalonia.Threading;
using Microsoft.Toolkit.Uwp.Notifications; using Microsoft.Toolkit.Uwp.Notifications;
using ReactiveUI;
namespace Artemis.UI.Windows.Providers; namespace Artemis.UI.Windows.Providers;
@ -20,18 +18,34 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller; private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
private readonly IMainWindowService _mainWindowService; private readonly IMainWindowService _mainWindowService;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private readonly IWorkshopUpdateService _workshopUpdateService;
private readonly IRouter _router; private readonly IRouter _router;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, IRouter router, Func<Guid, ReleaseInstaller> getReleaseInstaller) public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
IUpdateService updateService,
IWorkshopUpdateService workshopUpdateService,
IRouter router, Func<Guid, ReleaseInstaller> getReleaseInstaller)
{ {
_mainWindowService = mainWindowService; _mainWindowService = mainWindowService;
_updateService = updateService; _updateService = updateService;
_workshopUpdateService = workshopUpdateService;
_router = router; _router = router;
_getReleaseInstaller = getReleaseInstaller; _getReleaseInstaller = getReleaseInstaller;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
} }
/// <inheritdoc />
public void ShowWorkshopNotification(int updatedEntries)
{
new ToastContentBuilder().AddText(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed")
.AddText(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed")
.AddArgument("action", "view-library")
.AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-library"))
.AddButton(new ToastButton().SetContent("Don't show again").AddArgument("action", "disable-workshop-notifications"))
.Show();
}
/// <inheritdoc /> /// <inheritdoc />
public void ShowNotification(Guid releaseId, string releaseVersion) public void ShowNotification(Guid releaseId, string releaseVersion)
{ {
@ -57,14 +71,8 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
private void ViewRelease(Guid? releaseId) private void ViewRelease(Guid? releaseId)
{ {
Dispatcher.UIThread.Invoke(async () => string route = releaseId != null && releaseId.Value != Guid.Empty ? $"settings/releases/{releaseId}" : "settings/releases";
{ NavigateToRoute(route);
_mainWindowService.OpenMainWindow();
if (releaseId != null && releaseId.Value != Guid.Empty)
await _router.Navigate($"settings/releases/{releaseId}");
else
await _router.Navigate("settings/releases");
});
} }
private async Task InstallRelease(Guid releaseId, string releaseVersion) private async Task InstallRelease(Guid releaseId, string releaseVersion)
@ -153,11 +161,9 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
ToastArguments args = ToastArguments.Parse(e.Argument); ToastArguments args = ToastArguments.Parse(e.Argument);
Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty; Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty;
string releaseVersion = args.Get("releaseVersion"); string releaseVersion = args.Contains("releaseVersion") ? args.Get("releaseVersion") : string.Empty;
string action = "view-changes"; string action = args.Contains("action") ? args.Get("action") : "view-changes";
if (args.Contains("action"))
action = args.Get("action");
if (action == "install") if (action == "install")
await InstallRelease(releaseId, releaseVersion); await InstallRelease(releaseId, releaseVersion);
else if (action == "view-changes") else if (action == "view-changes")
@ -166,5 +172,18 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
else if (action == "restart-for-update") else if (action == "restart-for-update")
_updateService.RestartForUpdate("WindowsNotification", false); _updateService.RestartForUpdate("WindowsNotification", false);
else if (action == "disable-workshop-notifications")
_workshopUpdateService.DisableNotifications();
else if (action == "view-library")
NavigateToRoute("workshop/library");
}
private void NavigateToRoute(string route)
{
Dispatcher.UIThread.Invoke(async () =>
{
_mainWindowService.OpenMainWindow();
await _router.Navigate(route);
});
} }
} }

View File

@ -194,7 +194,7 @@
Auto-install updates Auto-install updates
</TextBlock> </TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap"> <TextBlock Classes="subtitle" TextWrapping="Wrap">
If enabled, new updates will automatically be installed. Automatically install new versions of Artemis in the background when available.
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center"> <StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
@ -202,6 +202,21 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto" IsVisible="{CompiledBinding IsWindows}">
<StackPanel Grid.Column="0">
<TextBlock>
Show workshop update notifications
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
Show a desktop notification whenever workshop updates are installed.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsChecked="{CompiledBinding WorkshopShowNotifications.Value}" MinWidth="0" OnContent="Yes" OffContent="No" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto"> <Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center"> <StackPanel Grid.Column="0" VerticalAlignment="Center">

View File

@ -102,6 +102,7 @@ public class GeneralTabViewModel : RoutableScreen
public bool IsAutoRunSupported => _autoRunProvider != null; public bool IsAutoRunSupported => _autoRunProvider != null;
public bool IsWindows11 => OSVersionHelper.IsWindows11(); public bool IsWindows11 => OSVersionHelper.IsWindows11();
public bool IsWindows => OSVersionHelper.IsWindows();
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; } public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
public ObservableCollection<string> GraphicsContexts { get; } public ObservableCollection<string> GraphicsContexts { get; }
@ -158,6 +159,7 @@ public class GeneralTabViewModel : RoutableScreen
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
public PluginSetting<bool> EnableMica => _settingsService.GetSetting("UI.EnableMica", true); public PluginSetting<bool> EnableMica => _settingsService.GetSetting("UI.EnableMica", true);
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
public PluginSetting<bool> WorkshopShowNotifications => _settingsService.GetSetting("Workshop.ShowNotifications", true);
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true); public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);

View File

@ -101,14 +101,27 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
return; return;
} }
// If not the latest version, warn and offer to disable auto-updates
bool disableAutoUpdates = false;
if (Release.Id != Release.Entry.LatestReleaseId)
{
disableAutoUpdates = await _windowService.ShowConfirmContentDialog(
"You are installing an older version of this entry",
"Would you like to disable auto-updates for this entry?",
"Yes",
"No"
);
}
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
InstallProgress = 0; InstallProgress = 0;
InstallationInProgress = true; InstallationInProgress = true;
try try
{ {
EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token); EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token);
if (result.IsSuccess) if (result.IsSuccess && result.Entry != null)
{ {
_workshopService.SetAutoUpdate(result.Entry, !disableAutoUpdates);
_notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show(); _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
InstallationInProgress = false; InstallationInProgress = false;
await Manage(); await Manage();
@ -156,7 +169,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
await UninstallPluginPrerequisites(installedEntry); await UninstallPluginPrerequisites(installedEntry);
await _workshopService.UninstallEntry(installedEntry, CancellationToken.None); await _workshopService.UninstallEntry(installedEntry, CancellationToken.None);
_notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show(); _notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show();
} }
finally finally

View File

@ -66,11 +66,9 @@
<!-- Info --> <!-- Info -->
<StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right"> <StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right"> <TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Downloads" /> <avalonia:MaterialIcon Kind="Harddisk" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" /> <Run Text="{CompiledBinding Entry.ReleaseVersion}" />
<Run>downloads</Run>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
@ -89,7 +87,7 @@
<Button Command="{CompiledBinding ViewLocal}" HorizontalAlignment="Stretch" >Open</Button> <Button Command="{CompiledBinding ViewLocal}" HorizontalAlignment="Stretch" >Open</Button>
<Button Command="{CompiledBinding Uninstall}" HorizontalAlignment="Stretch">Uninstall</Button> <Button Command="{CompiledBinding Uninstall}" HorizontalAlignment="Stretch">Uninstall</Button>
</StackPanel> </StackPanel>
<CheckBox MinHeight="26" Margin="0 4 0 0" IsEnabled="False">Auto-update</CheckBox> <CheckBox MinHeight="26" Margin="0 4 0 0" IsChecked="{CompiledBinding AutoUpdate}">Auto-update</CheckBox>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View File

@ -1,7 +1,4 @@
using Avalonia; using Avalonia.ReactiveUI;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;

View File

@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
@ -9,6 +10,7 @@ using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
@ -25,16 +27,19 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly IWorkshopUpdateService _workshopUpdateService;
private readonly IRouter _router; private readonly IRouter _router;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory; private readonly ISettingsVmFactory _settingsVmFactory;
[Notify] private bool _updateAvailable; [Notify] private bool _updateAvailable;
[Notify] private bool _autoUpdate;
public InstalledTabItemViewModel(InstalledEntry entry, public InstalledTabItemViewModel(InstalledEntry entry,
IWorkshopClient client, IWorkshopClient client,
IWorkshopService workshopService, IWorkshopService workshopService,
IWorkshopUpdateService workshopUpdateService,
IRouter router, IRouter router,
IWindowService windowService, IWindowService windowService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
@ -42,10 +47,13 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase
{ {
_client = client; _client = client;
_workshopService = workshopService; _workshopService = workshopService;
_workshopUpdateService = workshopUpdateService;
_router = router; _router = router;
_windowService = windowService; _windowService = windowService;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_settingsVmFactory = settingsVmFactory; _settingsVmFactory = settingsVmFactory;
_autoUpdate = entry.AutoUpdate;
Entry = entry; Entry = entry;
this.WhenActivatedAsync(async _ => this.WhenActivatedAsync(async _ =>
@ -65,6 +73,8 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase
UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId; UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId;
} }
}); });
this.WhenAnyValue(vm => vm.AutoUpdate).Skip(1).Subscribe(_ => AutoUpdateToggled());
} }
public InstalledEntry Entry { get; } public InstalledEntry Entry { get; }
@ -108,4 +118,18 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
await pluginViewModel.ExecuteRemovePrerequisites(true); await pluginViewModel.ExecuteRemovePrerequisites(true);
} }
private void AutoUpdateToggled()
{
_workshopService.SetAutoUpdate(Entry, AutoUpdate);
if (!AutoUpdate)
return;
Task.Run(async () =>
{
await _workshopUpdateService.AutoUpdateEntry(Entry);
UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId;
});
}
} }

View File

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Artemis.WebClient.Workshop.Models;
namespace Artemis.UI.Services.Interfaces;
public interface IWorkshopUpdateService : IArtemisUIService
{
/// <summary>
/// Automatically updates all installed entries that have auto-update enabled and have a new version available.
/// </summary>
/// <returns>A task that represents the asynchronous operation</returns>
Task AutoUpdateEntries();
/// <summary>
/// Automatically updates the provided entry if a new version is available.
/// </summary>
/// <param name="entry">The entry to update.</param>
/// <returns>A task of <see langword="true"/> if the entry was updated, <see langword="false"/> otherwise.</returns>
Task<bool> AutoUpdateEntry(InstalledEntry entry);
/// <summary>
/// Disable workshop update notifications.
/// </summary>
void DisableNotifications();
}

View File

@ -57,6 +57,18 @@ public class BasicUpdateNotificationProvider : IUpdateNotificationProvider
await _router.Navigate("settings/releases"); await _router.Navigate("settings/releases");
} }
/// <inheritdoc />
public void ShowWorkshopNotification(int updatedEntries)
{
_notificationService.CreateNotification()
.WithTitle(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed")
.WithMessage(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed")
.WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View library").WithAction(async () => await _router.Navigate("settings/workshop")))
.Show();
}
/// <inheritdoc /> /// <inheritdoc />
public void ShowNotification(Guid releaseId, string releaseVersion) public void ShowNotification(Guid releaseId, string releaseVersion)
{ {

View File

@ -4,6 +4,7 @@ namespace Artemis.UI.Services.Updating;
public interface IUpdateNotificationProvider public interface IUpdateNotificationProvider
{ {
void ShowWorkshopNotification(int updatedEntries);
void ShowNotification(Guid releaseId, string releaseVersion); void ShowNotification(Guid releaseId, string releaseVersion);
void ShowInstalledNotification(string installedVersion); void ShowInstalledNotification(string installedVersion);
} }

View File

@ -8,8 +8,10 @@ using Artemis.Core.Services;
using Artemis.Storage.Repositories; using Artemis.Storage.Repositories;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Exceptions; using Artemis.UI.Exceptions;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Artemis.WebClient.Updating; using Artemis.WebClient.Updating;
using Artemis.WebClient.Workshop.Services;
using Serilog; using Serilog;
using StrawberryShake; using StrawberryShake;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
@ -26,6 +28,7 @@ public class UpdateService : IUpdateService
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService; private readonly IMainWindowService _mainWindowService;
private readonly IReleaseRepository _releaseRepository; private readonly IReleaseRepository _releaseRepository;
private readonly IWorkshopUpdateService _workshopUpdateService;
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider; private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
private readonly Platform _updatePlatform; private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient; private readonly IUpdatingClient _updatingClient;
@ -38,6 +41,7 @@ public class UpdateService : IUpdateService
IMainWindowService mainWindowService, IMainWindowService mainWindowService,
IUpdatingClient updatingClient, IUpdatingClient updatingClient,
IReleaseRepository releaseRepository, IReleaseRepository releaseRepository,
IWorkshopUpdateService workshopUpdateService,
Lazy<IUpdateNotificationProvider> updateNotificationProvider, Lazy<IUpdateNotificationProvider> updateNotificationProvider,
Func<Guid, ReleaseInstaller> getReleaseInstaller) Func<Guid, ReleaseInstaller> getReleaseInstaller)
{ {
@ -45,6 +49,7 @@ public class UpdateService : IUpdateService
_mainWindowService = mainWindowService; _mainWindowService = mainWindowService;
_updatingClient = updatingClient; _updatingClient = updatingClient;
_releaseRepository = releaseRepository; _releaseRepository = releaseRepository;
_workshopUpdateService = workshopUpdateService;
_updateNotificationProvider = updateNotificationProvider; _updateNotificationProvider = updateNotificationProvider;
_getReleaseInstaller = getReleaseInstaller; _getReleaseInstaller = getReleaseInstaller;
@ -65,72 +70,7 @@ public class UpdateService : IUpdateService
timer.Elapsed += HandleAutoUpdateEvent; timer.Elapsed += HandleAutoUpdateEvent;
timer.Start(); timer.Start();
} }
private void ProcessReleaseStatus()
{
string currentVersion = Constants.CurrentVersion;
bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion);
PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version;
if (!Directory.Exists(Constants.UpdatingFolder))
return;
// Clean up the update folder, leaving only the last ZIP
foreach (string file in Directory.GetFiles(Constants.UpdatingFolder))
{
if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip")
continue;
try
{
_logger.Debug("Cleaning up old update file at {FilePath}", file);
File.Delete(file);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to clean up old update file at {FilePath}", file);
}
}
if (updated)
_updateNotificationProvider.Value.ShowInstalledNotification(currentVersion);
}
private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
{
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
}
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
{
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
await installer.InstallAsync(CancellationToken.None);
RestartForUpdate("AutoInstallUpdate", true);
}
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
{
if (Constants.CurrentVersion == "local")
return;
// The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes
if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59))
return;
_lastAutoUpdateCheck = DateTime.UtcNow;
if (!_autoCheck.Value || _suspendAutoCheck)
return;
try
{
await CheckForUpdate();
}
catch (Exception ex)
{
_logger.Warning(ex, "Auto update-check failed");
}
}
/// <inheritdoc /> /// <inheritdoc />
public string Channel { get; private set; } = "master"; public string Channel { get; private set; } = "master";
@ -139,7 +79,7 @@ public class UpdateService : IUpdateService
/// <inheritdoc /> /// <inheritdoc />
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public async Task CacheLatestRelease() public async Task CacheLatestRelease()
{ {
@ -257,4 +197,86 @@ public class UpdateService : IUpdateService
_logger.Information("Update service initialized for {Channel} channel", Channel); _logger.Information("Update service initialized for {Channel} channel", Channel);
return false; return false;
} }
private async Task<bool> AutoCheckForUpdates()
{
// Don't perform auto-updates if the current version is local
if (Constants.CurrentVersion == "local")
return false;
// Don't perform auto-updates if the setting is disabled or an update was found but not yet installed
if (!_autoCheck.Value || _suspendAutoCheck)
return false;
try
{
return await CheckForUpdate() && _autoInstall.Value;
}
catch (Exception ex)
{
_logger.Warning(ex, "Auto update-check failed");
}
return false;
}
private void ProcessReleaseStatus()
{
string currentVersion = Constants.CurrentVersion;
bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion);
PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version;
if (!Directory.Exists(Constants.UpdatingFolder))
return;
// Clean up the update folder, leaving only the last ZIP
foreach (string file in Directory.GetFiles(Constants.UpdatingFolder))
{
if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip")
continue;
try
{
_logger.Debug("Cleaning up old update file at {FilePath}", file);
File.Delete(file);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to clean up old update file at {FilePath}", file);
}
}
if (updated)
_updateNotificationProvider.Value.ShowInstalledNotification(currentVersion);
}
private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
{
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
}
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
{
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
await installer.InstallAsync(CancellationToken.None);
RestartForUpdate("AutoInstallUpdate", true);
}
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
{
// The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes
if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59))
return;
_lastAutoUpdateCheck = DateTime.UtcNow;
if (await AutoCheckForUpdates())
{
_logger.Information("Auto-installing update, not performing workshop update check");
}
else
{
await _workshopUpdateService.AutoUpdateEntries();
}
}
} }

View File

@ -0,0 +1,89 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Services.Updating;
public class WorkshopUpdateService : IWorkshopUpdateService
{
private readonly ILogger _logger;
private readonly IWorkshopClient _client;
private readonly INotificationService _notificationService;
private readonly IWorkshopService _workshopService;
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
private readonly PluginSetting<bool> _showNotifications;
public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService,
Lazy<IUpdateNotificationProvider> updateNotificationProvider)
{
_logger = logger;
_client = client;
_workshopService = workshopService;
_updateNotificationProvider = updateNotificationProvider;
_showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true);
}
public async Task AutoUpdateEntries()
{
_logger.Information("Checking for workshop updates");
int checkedEntries = 0;
int updatedEntries = 0;
foreach (InstalledEntry entry in _workshopService.GetInstalledEntries())
{
if (!entry.AutoUpdate)
continue;
checkedEntries++;
bool updated = await AutoUpdateEntry(entry);
if (updated)
updatedEntries++;
}
_logger.Information("Checked {CheckedEntries} entries, updated {UpdatedEntries}", checkedEntries, updatedEntries);
if (updatedEntries > 0 && _showNotifications.Value)
_updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries);
}
public async Task<bool> AutoUpdateEntry(InstalledEntry entry)
{
// Query the latest version
IOperationResult<IGetEntryLatestReleaseByIdResult> latestReleaseResult = await _client.GetEntryLatestReleaseById.ExecuteAsync(entry.Id);
if (latestReleaseResult.Data?.Entry?.LatestRelease is not IRelease latestRelease)
return false;
if (latestRelease.Id == entry.ReleaseId)
return false;
_logger.Information("Auto-updating entry {Entry} to version {Version}", entry, latestRelease.Version);
EntryInstallResult updateResult = await _workshopService.InstallEntry(entry, latestRelease, new Progress<StreamProgress>(), CancellationToken.None);
// This happens during installation too but not on our reference of the entry
if (updateResult.IsSuccess)
entry.ApplyRelease(latestRelease);
_logger.Information("Auto-update result: {Result}", updateResult);
return updateResult.IsSuccess;
}
/// <inheritdoc />
public void DisableNotifications()
{
_showNotifications.Value = false;
_showNotifications.Save();
}
}

View File

@ -26,4 +26,10 @@ public class EntryInstallResult
Entry = installedEntry Entry = installedEntry
}; };
} }
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(IsSuccess)}: {IsSuccess}, {nameof(Message)}: {Message}";
}
} }

View File

@ -30,7 +30,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
{ {
// If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done // If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done
if (installedEntry.GetReleaseDirectory(release).Exists) if (installedEntry.GetReleaseDirectory(release).Exists)
return EntryInstallResult.FromSuccess(installedEntry); return ApplyAndSave(installedEntry, release);
} }
else else
{ {
@ -102,10 +102,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
return EntryInstallResult.FromFailure(e.Message); return EntryInstallResult.FromFailure(e.Message);
} }
installedEntry.ApplyRelease(release); return ApplyAndSave(installedEntry, release);
_workshopService.SaveInstalledEntry(installedEntry);
return EntryInstallResult.FromSuccess(installedEntry);
} }
public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
@ -135,4 +132,11 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
_workshopService.RemoveInstalledEntry(installedEntry); _workshopService.RemoveInstalledEntry(installedEntry);
return Task.FromResult(EntryUninstallResult.FromSuccess(message)); return Task.FromResult(EntryUninstallResult.FromSuccess(message));
} }
private EntryInstallResult ApplyAndSave(InstalledEntry installedEntry, IRelease release)
{
installedEntry.ApplyRelease(release);
_workshopService.SaveInstalledEntry(installedEntry);
return EntryInstallResult.FromSuccess(installedEntry);
}
} }

View File

@ -18,6 +18,9 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary
private DateTimeOffset _createdAt; private DateTimeOffset _createdAt;
private long? _latestReleaseId; private long? _latestReleaseId;
private IReadOnlyList<IGetDependantEntries_Entries_Items_Categories> _categories; private IReadOnlyList<IGetDependantEntries_Entries_Items_Categories> _categories;
private long _releaseId;
private string _releaseVersion = string.Empty;
private bool _autoUpdate;
internal InstalledEntry(EntryEntity entity) internal InstalledEntry(EntryEntity entity)
{ {
@ -36,12 +39,26 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary
AutoUpdate = true; AutoUpdate = true;
} }
public long ReleaseId { get; set; }
public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; }
public bool AutoUpdate { get; set; }
internal EntryEntity Entity { get; } internal EntryEntity Entity { get; }
public DateTimeOffset InstalledAt { get; set; }
public long ReleaseId
{
get => _releaseId;
set => SetAndNotify(ref _releaseId, value);
}
public string ReleaseVersion
{
get => _releaseVersion;
set => SetAndNotify(ref _releaseVersion, value);
}
public bool AutoUpdate
{
get => _autoUpdate;
set => SetAndNotify(ref _autoUpdate, value);
}
internal void Load() internal void Load()
{ {
@ -233,4 +250,10 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary
} }
#endregion #endregion
/// <inheritdoc />
public override string ToString()
{
return $"[{EntryType}] {Id} - {Name}";
}
} }

View File

@ -32,4 +32,12 @@ query GetEntrySummaryById($id: Long!) {
entry(id: $id) { entry(id: $id) {
...entrySummary ...entrySummary
} }
}
query GetEntryLatestReleaseById($id: Long!) {
entry(id: $id) {
latestRelease {
...releaseDetails
}
}
} }

View File

@ -133,5 +133,6 @@ public interface IWorkshopService
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved; public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
public event EventHandler<InstalledEntry>? OnEntryUninstalled; public event EventHandler<InstalledEntry>? OnEntryUninstalled;
public event EventHandler<InstalledEntry>? OnEntryInstalled; public event EventHandler<InstalledEntry>? OnEntryInstalled;
void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate);
} }

View File

@ -165,7 +165,7 @@ public class WorkshopService : IWorkshopService
if (result.IsSuccess && result.Entry != null) if (result.IsSuccess && result.Entry != null)
OnEntryInstalled?.Invoke(this, result.Entry); OnEntryInstalled?.Invoke(this, result.Entry);
else else
_logger.Warning("Failed to install entry {EntryId}: {Message}", entry.Id, result.Message); _logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message);
return result; return result;
} }
@ -254,6 +254,16 @@ public class WorkshopService : IWorkshopService
} }
} }
/// <inheritdoc />
public void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate)
{
if (installedEntry.AutoUpdate == autoUpdate)
return;
installedEntry.AutoUpdate = autoUpdate;
SaveInstalledEntry(installedEntry);
}
private void RemoveOrphanedFiles() private void RemoveOrphanedFiles()
{ {
List<InstalledEntry> entries = GetInstalledEntries(); List<InstalledEntry> entries = GetInstalledEntries();
@ -308,6 +318,8 @@ public class WorkshopService : IWorkshopService
} }
public event EventHandler<InstalledEntry>? OnInstalledEntrySaved; public event EventHandler<InstalledEntry>? OnInstalledEntrySaved;
public event EventHandler<InstalledEntry>? OnEntryUninstalled; public event EventHandler<InstalledEntry>? OnEntryUninstalled;
public event EventHandler<InstalledEntry>? OnEntryInstalled; public event EventHandler<InstalledEntry>? OnEntryInstalled;
} }