diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index ccbbff9ea..f6716082e 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -63,7 +63,6 @@ internal class CoreService : ICoreService _logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion); // Initialize the services - _pluginManagementService.CopyBuiltInPlugins(); _pluginManagementService.LoadPlugins(IsElevated); _pluginManagementService.StartHotReload(); _renderService.Initialize(); diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 806526773..b6017a5c6 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -21,13 +21,12 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// Indicates whether or not plugins are currently being loaded /// bool LoadingPlugins { get; } - + /// - /// Copy built-in plugins from the executable directory to the plugins directory if the version is higher - /// (higher or equal if compiled as debug) + /// Indicates whether or not plugins are currently loaded /// - void CopyBuiltInPlugins(); - + bool LoadedPlugins { get; } + /// /// Loads all installed plugins. If plugins already loaded this will reload them all /// @@ -150,12 +149,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// /// DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); - - /// - /// Occurs when built-in plugins are being loaded - /// - event EventHandler CopyingBuildInPlugins; - + /// /// Occurs when a plugin has started loading /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 346d82a88..f65a9d858 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -46,114 +46,8 @@ internal class PluginManagementService : IPluginManagementService public List AdditionalPluginDirectories { get; } = new(); public bool LoadingPlugins { get; private set; } - - - #region Built in plugins - - public void CopyBuiltInPlugins() - { - OnCopyingBuildInPlugins(); - DirectoryInfo pluginDirectory = new(Constants.PluginsFolder); - - if (Directory.Exists(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.Modules.Overlay-29e3ff97"))) - Directory.Delete(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.Modules.Overlay-29e3ff97"), true); - if (Directory.Exists(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.DataModelExpansions.TestData-ab41d601"))) - Directory.Delete(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.DataModelExpansions.TestData-ab41d601"), true); - - // Iterate built-in plugins - DirectoryInfo builtInPluginDirectory = new(Path.Combine(Constants.ApplicationFolder, "Plugins")); - if (!builtInPluginDirectory.Exists) - { - _logger.Warning("No built-in plugins found at {pluginDir}, skipping CopyBuiltInPlugins", builtInPluginDirectory.FullName); - return; - } - - - foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) - { - try - { - ExtractBuiltInPlugin(zipFile, pluginDirectory); - } - catch (Exception e) - { - _logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName); - } - } - } - - private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) - { - // Find the metadata file in the zip - using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); - - ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json"); - if (metaDataFileEntry == null) - throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName); - - using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo builtInPluginInfo = CoreJson.Deserialize(reader.ReadToEnd())!; - string preferred = builtInPluginInfo.PreferredPluginDirectory; - - // Find the matching plugin in the plugin folder - DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); - if (match == null) - { - CopyBuiltInPlugin(archive, preferred); - } - else - { - string metadataFile = Path.Combine(match.FullName, "plugin.json"); - if (!File.Exists(metadataFile)) - { - _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } - else if (metaDataFileEntry.LastWriteTime > File.GetLastWriteTime(metadataFile)) - { - try - { - _logger.Debug("Copying updated built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } - catch (Exception e) - { - throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e); - } - } - } - } - - private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) - { - ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json"); - DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory)); - bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); - - // Remove the old directory if it exists - if (Directory.Exists(pluginDirectory.FullName)) - pluginDirectory.Delete(true); - - // Extract everything in the same archive directory to the unique plugin directory - Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); - string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); - foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) - { - if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/")) - { - string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); - // Create folders - Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); - // Extract files - zipArchiveEntry.ExtractToFile(target); - } - } - - if (createLockFile) - File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); - } - - #endregion + + public bool LoadedPlugins { get; private set; } public List GetAllPlugins() { @@ -328,8 +222,10 @@ internal class PluginManagementService : IPluginManagementService // ReSharper restore InconsistentlySynchronizedField LoadingPlugins = false; + LoadedPlugins = true; } + public void UnloadPlugins() { // Unload all plugins @@ -686,7 +582,7 @@ internal class PluginManagementService : IPluginManagementService if (removeSettings) RemovePluginSettings(plugin); - + OnPluginRemoved(new PluginEventArgs(plugin)); } @@ -893,7 +789,7 @@ internal class PluginManagementService : IPluginManagementService { PluginDisabled?.Invoke(this, e); } - + protected virtual void OnPluginRemoved(PluginEventArgs e) { PluginRemoved?.Invoke(this, e); diff --git a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs index c29963248..82d1f4b86 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs @@ -6,11 +6,11 @@ using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Exceptions; +using Artemis.UI.Services; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; using Avalonia.Controls; using Avalonia.Threading; using Material.Icons; @@ -21,9 +21,7 @@ namespace Artemis.UI.Screens.Plugins; public partial class PluginViewModel : ActivatableViewModelBase { - private readonly ICoreService _coreService; - private readonly INotificationService _notificationService; - private readonly IPluginManagementService _pluginManagementService; + private readonly IPluginInteractionService _pluginInteractionService; private readonly IWindowService _windowService; private Window? _settingsWindow; [Notify] private bool _canInstallPrerequisites; @@ -31,18 +29,11 @@ public partial class PluginViewModel : ActivatableViewModelBase [Notify] private bool _enabling; [Notify] private Plugin _plugin; - public PluginViewModel(Plugin plugin, - ReactiveCommand? reload, - ICoreService coreService, - IWindowService windowService, - INotificationService notificationService, - IPluginManagementService pluginManagementService) + public PluginViewModel(Plugin plugin, ReactiveCommand? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService) { _plugin = plugin; - _coreService = coreService; _windowService = windowService; - _notificationService = notificationService; - _pluginManagementService = pluginManagementService; + _pluginInteractionService = pluginInteractionService; Platforms = new ObservableCollection(); if (Plugin.Info.Platforms != null) @@ -88,7 +79,6 @@ public partial class PluginViewModel : ActivatableViewModelBase public ReactiveCommand OpenPluginDirectory { get; } public ObservableCollection Platforms { get; } - public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; public bool IsEnabled => Plugin.IsEnabled; public async Task UpdateEnabled(bool enable) @@ -97,55 +87,15 @@ public partial class PluginViewModel : ActivatableViewModelBase return; if (!enable) - { - try - { - await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true)); - } - catch (Exception e) - { - await ShowUpdateEnableFailure(enable, e); - } - finally - { - this.RaisePropertyChanged(nameof(IsEnabled)); - } - - return; - } - - try + await _pluginInteractionService.DisablePlugin(Plugin); + else { Enabling = true; - if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated) - { - bool confirmed = await _windowService.ShowConfirmContentDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it? Artemis will need to restart.", "Confirm and restart"); - if (!confirmed) - return; - } - - // Check if all prerequisites are met async - List subjects = new() {Plugin.Info}; - subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); - - if (subjects.Any(s => !s.ArePrerequisitesMet())) - { - await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); - if (!subjects.All(s => s.ArePrerequisitesMet())) - return; - } - - await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true)); - } - catch (Exception e) - { - await ShowUpdateEnableFailure(enable, e); - } - finally - { + await _pluginInteractionService.EnablePlugin(Plugin, false); Enabling = false; - this.RaisePropertyChanged(nameof(IsEnabled)); } + + this.RaisePropertyChanged(nameof(IsEnabled)); } public void CheckPrerequisites() @@ -220,43 +170,12 @@ public partial class PluginViewModel : ActivatableViewModelBase private async Task ExecuteRemoveSettings() { - bool confirmed = await _windowService.ShowConfirmContentDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); - if (!confirmed) - return; - - bool wasEnabled = IsEnabled; - - if (IsEnabled) - await UpdateEnabled(false); - - _pluginManagementService.RemovePluginSettings(Plugin); - - if (wasEnabled) - await UpdateEnabled(true); - - _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); + await _pluginInteractionService.RemovePluginSettings(Plugin); } private async Task ExecuteRemove() { - bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); - if (!confirmed) - return; - - // If the plugin or any of its features has uninstall actions, offer to run these - await ExecuteRemovePrerequisites(true); - - try - { - _pluginManagementService.RemovePlugin(Plugin, false); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Failed to remove plugin", e); - throw; - } - - _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); + await _pluginInteractionService.RemovePlugin(Plugin); } private void ExecuteShowLogsFolder() @@ -271,20 +190,6 @@ public partial class PluginViewModel : ActivatableViewModelBase } } - private async Task ShowUpdateEnableFailure(bool enable, Exception e) - { - string action = enable ? "enable" : "disable"; - ContentDialogBuilder builder = _windowService.CreateContentDialog() - .WithTitle($"Failed to {action} plugin {Plugin.Info.Name}") - .WithContent(e.Message) - .HavingPrimaryButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)); - // If available, add a secondary button pointing to the support page - if (Plugin.Info.HelpPage != null) - builder = builder.HavingSecondaryButton(b => b.WithText("Open support page").WithAction(() => Utilities.OpenUrl(Plugin.Info.HelpPage.ToString()))); - - await builder.ShowAsync(); - } - private void OnPluginToggled(object? sender, EventArgs e) { Dispatcher.UIThread.Post(() => @@ -299,9 +204,9 @@ public partial class PluginViewModel : ActivatableViewModelBase { if (IsEnabled) return; - + await UpdateEnabled(true); - + // If enabling failed, don't offer to show the settings if (!IsEnabled || Plugin.ConfigurationDialog == null) return; diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index df894ec9b..b3433b5da 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -83,7 +83,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv _coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow); } - Task.Run(() => + Task.Run(async () => { try { @@ -93,7 +93,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv return; // Workshop service goes first so it has a chance to clean up old workshop entries and introduce new ones - workshopService.Initialize(); + await workshopService.Initialize(); // Core is initialized now that everything is ready to go coreService.Initialize(); diff --git a/src/Artemis.UI/Screens/Root/SplashViewModel.cs b/src/Artemis.UI/Screens/Root/SplashViewModel.cs index dd9622421..2900d9948 100644 --- a/src/Artemis.UI/Screens/Root/SplashViewModel.cs +++ b/src/Artemis.UI/Screens/Root/SplashViewModel.cs @@ -2,6 +2,7 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; +using Artemis.WebClient.Workshop.Services; using PropertyChanged.SourceGenerator; namespace Artemis.UI.Screens.Root; @@ -10,12 +11,12 @@ public partial class SplashViewModel : ViewModelBase { [Notify] private string _status; - public SplashViewModel(ICoreService coreService, IPluginManagementService pluginManagementService) + public SplashViewModel(ICoreService coreService, IPluginManagementService pluginManagementService, IWorkshopService workshopService) { CoreService = coreService; _status = "Initializing Core"; - pluginManagementService.CopyingBuildInPlugins += OnPluginManagementServiceOnCopyingBuildInPluginsManagement; + workshopService.MigratingBuildInPlugins += WorkshopServiceOnMigratingBuildInPlugins; pluginManagementService.PluginLoading += OnPluginManagementServiceOnPluginManagementLoading; pluginManagementService.PluginLoaded += OnPluginManagementServiceOnPluginManagementLoaded; pluginManagementService.PluginEnabling += PluginManagementServiceOnPluginManagementEnabling; @@ -25,6 +26,11 @@ public partial class SplashViewModel : ViewModelBase } public ICoreService CoreService { get; } + + private void WorkshopServiceOnMigratingBuildInPlugins(object? sender, EventArgs args) + { + Status = "Migrating built-in plugins"; + } private void OnPluginManagementServiceOnPluginManagementLoaded(object? sender, PluginEventArgs args) { @@ -55,9 +61,4 @@ public partial class SplashViewModel : ViewModelBase { Status = "Initializing UI"; } - - private void OnPluginManagementServiceOnCopyingBuildInPluginsManagement(object? sender, EventArgs args) - { - Status = "Updating built-in plugins"; - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs index b8382fbac..44b59b947 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs @@ -5,9 +5,10 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.DryIoc.Factories; using Artemis.UI.Exceptions; using Artemis.UI.Screens.Plugins; +using Artemis.UI.Services; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Utilities; @@ -28,7 +29,7 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase private readonly IWindowService _windowService; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; - private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IPluginInteractionService _pluginInteractionService; private readonly Progress _progress = new(); [Notify] private bool _isInstalled; @@ -41,14 +42,14 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase IWindowService windowService, IPluginManagementService pluginManagementService, IProfileService profileService, - ISettingsVmFactory settingsVmFactory) + IPluginInteractionService pluginInteractionService) { _logger = logger; _workshopService = workshopService; _windowService = windowService; _pluginManagementService = pluginManagementService; _profileService = profileService; - _settingsVmFactory = settingsVmFactory; + _pluginInteractionService = pluginInteractionService; Entry = entry; _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; @@ -62,10 +63,7 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase if (IsInstalled || !ShouldInstall || Entry.LatestRelease == null) return true; - // Most entries install so fast it looks broken without a small delay - Task minimumDelay = Task.Delay(100, cancellationToken); EntryInstallResult result = await _workshopService.InstallEntry(Entry, Entry.LatestRelease, _progress, cancellationToken); - await minimumDelay; if (!result.IsSuccess) { @@ -95,8 +93,7 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase throw new InvalidOperationException($"Plugin with id '{pluginId}' does not exist."); // There's quite a bit of UI involved in enabling a plugin, borrowing the PluginSettingsViewModel for this - PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); - await pluginViewModel.UpdateEnabled(true); + await _pluginInteractionService.EnablePlugin(plugin, true); // Find features without prerequisites to enable foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features) @@ -113,15 +110,6 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase _logger.Warning(e, "Failed to enable plugin feature '{FeatureName}', skipping", pluginFeatureInfo.Name); } } - - // If the plugin has a mandatory settings window, open it and wait - if (plugin.ConfigurationDialog != null && plugin.ConfigurationDialog.IsMandatory) - { - if (plugin.Resolve(plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) - throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); - - await _windowService.ShowDialogAsync(new PluginSettingsWindowViewModel(viewModel)); - } } private void PrepareProfile(InstalledEntry entry) diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs index 3d5cba990..d077e8fae 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.DryIoc.Factories; -using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Workshop.EntryReleases.Dialogs; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; @@ -30,7 +29,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase private readonly IWindowService _windowService; private readonly IWorkshopService _workshopService; private readonly IPluginManagementService _pluginManagementService; - private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IPluginInteractionService _pluginInteractionService; private readonly Progress _progress = new(); [Notify] private IReleaseDetails? _release; @@ -46,14 +45,14 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase IWindowService windowService, IWorkshopService workshopService, IPluginManagementService pluginManagementService, - ISettingsVmFactory settingsVmFactory) + IPluginInteractionService pluginInteractionService) { _router = router; _notificationService = notificationService; _windowService = windowService; _workshopService = workshopService; _pluginManagementService = pluginManagementService; - _settingsVmFactory = settingsVmFactory; + _pluginInteractionService = pluginInteractionService; _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; this.WhenActivated(d => @@ -124,7 +123,10 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase _workshopService.SetAutoUpdate(result.Entry, !disableAutoUpdates); _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show(); InstallationInProgress = false; - await Manage(); + + // Auto-enable plugins as the installation handler won't deal with the required UI interactions + if (result.Installed is Plugin installedPlugin) + await AutoEnablePlugin(installedPlugin); } else if (!_cts.IsCancellationRequested) { @@ -141,12 +143,6 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase } } - public async Task Manage() - { - if (Release?.Entry.EntryType != EntryType.Profile) - await _router.Navigate("../../manage", new RouterNavigationOptions {AdditionalArguments = true}); - } - public async Task Reinstall() { if (await _windowService.ShowConfirmContentDialog("Reinstall entry", "Are you sure you want to reinstall this entry?")) @@ -193,7 +189,28 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase if (plugin == null) return; - PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); - await pluginViewModel.ExecuteRemovePrerequisites(true); + await _pluginInteractionService.RemovePluginPrerequisites(plugin, true); + } + + private async Task AutoEnablePlugin(Plugin plugin) + { + // There's quite a bit of UI involved in enabling a plugin, borrowing the PluginSettingsViewModel for this + await _pluginInteractionService.EnablePlugin(plugin, true); + + // Find features without prerequisites to enable + foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features) + { + if (pluginFeatureInfo.Instance == null || pluginFeatureInfo.Instance.IsEnabled || pluginFeatureInfo.Prerequisites.Count != 0) + continue; + + try + { + _pluginManagementService.EnablePluginFeature(pluginFeatureInfo.Instance, true); + } + catch (Exception e) + { + _notificationService.CreateNotification().WithTitle("Failed to enable plugin feature").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show(); + } + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs b/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs new file mode 100644 index 000000000..b76d765d6 --- /dev/null +++ b/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Artemis.Core; + +namespace Artemis.UI.Services.Interfaces; + +public interface IPluginInteractionService : IArtemisUIService +{ + /// + /// Enables a plugin, showing prerequisites and config windows as necessary. + /// + /// The plugin to enable. + /// + /// A task representing the asynchronous operation. + Task EnablePlugin(Plugin plugin, bool showMandatoryConfigWindow); + + /// + /// Disables a plugin, stopping all its features and services. + /// + /// The plugin to disable. + /// A task representing the asynchronous operation with a boolean indicating success. + Task DisablePlugin(Plugin plugin); + + /// + /// Removes a plugin from the system, optionally running uninstall actions for prerequisites. + /// + /// The plugin to remove. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePlugin(Plugin plugin); + + /// + /// Removes all settings and configuration data for a plugin, temporarily disabling it during the process. + /// + /// The plugin whose settings should be cleared. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePluginSettings(Plugin plugin); + + /// + /// Removes the prerequisites for a plugin. + /// + /// The plugin whose prerequisites should be removed. + /// Whether the prerequisites are being removed for a plugin removal. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePluginPrerequisites(Plugin plugin, bool forPluginRemoval); +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/PluginInteractionService.cs b/src/Artemis.UI/Services/PluginInteractionService.cs new file mode 100644 index 000000000..2314a2093 --- /dev/null +++ b/src/Artemis.UI/Services/PluginInteractionService.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; + +namespace Artemis.UI.Services; + +public class PluginInteractionService : IPluginInteractionService +{ + private readonly ICoreService _coreService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + private readonly INotificationService _notificationService; + + public PluginInteractionService(ICoreService coreService, IPluginManagementService pluginManagementService, IWindowService windowService, INotificationService notificationService) + { + _coreService = coreService; + _pluginManagementService = pluginManagementService; + _windowService = windowService; + _notificationService = notificationService; + } + + /// + public async Task EnablePlugin(Plugin plugin, bool showMandatoryConfigWindow) + { + try + { + if (plugin.Info.RequiresAdmin && !_coreService.IsElevated) + { + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Enable plugin", + "This plugin requires admin rights, are you sure you want to enable it? Artemis will need to restart.", + "Confirm and restart" + ); + if (!confirmed) + return false; + } + + // Check if all prerequisites are met async + List subjects = [plugin.Info]; + subjects.AddRange(plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); + + if (subjects.Any(s => !s.ArePrerequisitesMet())) + { + await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); + if (!subjects.All(s => s.ArePrerequisitesMet())) + return false; + } + + await Task.Run(() => _pluginManagementService.EnablePlugin(plugin, true, true)); + + // If the plugin has a mandatory settings window, open it and wait + if (showMandatoryConfigWindow && plugin.ConfigurationDialog != null && plugin.ConfigurationDialog.IsMandatory) + { + if (plugin.Resolve(plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) + throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); + + await _windowService.ShowDialogAsync(new PluginSettingsWindowViewModel(viewModel)); + } + + return true; + } + catch (Exception e) + { + await ShowPluginToggleFailure(plugin, true, e); + } + + return false; + } + + /// + public async Task DisablePlugin(Plugin plugin) + { + try + { + await Task.Run(() => _pluginManagementService.DisablePlugin(plugin, true)); + return true; + } + catch (Exception e) + { + await ShowPluginToggleFailure(plugin, false, e); + } + + return false; + } + + /// + public async Task RemovePlugin(Plugin plugin) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); + if (!confirmed) + return false; + + // If the plugin or any of its features has uninstall actions, offer to run these + await RemovePrerequisites(plugin, true); + + try + { + _pluginManagementService.RemovePlugin(plugin, false); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Failed to remove plugin", e); + throw; + } + + _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); + return true; + } + + /// + public async Task RemovePluginSettings(Plugin plugin) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); + if (!confirmed) + return false; + + bool wasEnabled = plugin.IsEnabled; + + if (wasEnabled) + _pluginManagementService.DisablePlugin(plugin, false); + + _pluginManagementService.RemovePluginSettings(plugin); + + if (wasEnabled) + _pluginManagementService.EnablePlugin(plugin, false); + + _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); + + return true; + } + + /// + public async Task RemovePluginPrerequisites(Plugin plugin, bool forPluginRemoval) + { + await RemovePrerequisites(plugin, false); + return true; + } + + private async Task ShowPluginToggleFailure(Plugin plugin, bool enable, Exception e) + { + string action = enable ? "enable" : "disable"; + ContentDialogBuilder builder = _windowService.CreateContentDialog() + .WithTitle($"Failed to {action} plugin {plugin.Info.Name}") + .WithContent(e.Message) + .HavingPrimaryButton(b => b.WithText("View logs").WithAction(() => Utilities.OpenFolder(Constants.LogsFolder))); + // If available, add a secondary button pointing to the support page + if (plugin.Info.HelpPage != null) + builder = builder.HavingSecondaryButton(b => b.WithText("Open support page").WithAction(() => Utilities.OpenUrl(plugin.Info.HelpPage.ToString()))); + + await builder.ShowAsync(); + } + + private async Task RemovePrerequisites(Plugin plugin, bool forPluginRemoval) + { + List subjects = [plugin.Info]; + subjects.AddRange(!forPluginRemoval ? plugin.Features.Where(f => f.AlwaysEnabled) : plugin.Features); + + if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) + await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs new file mode 100644 index 000000000..e6c29cffd --- /dev/null +++ b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs @@ -0,0 +1,106 @@ +using Artemis.Core; +using Artemis.Storage.Entities.Plugins; +using Artemis.Storage.Repositories.Interfaces; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop.Services; +using Serilog; +using StrawberryShake; + +namespace Artemis.WebClient.Workshop; + +public static class BuiltInPluginsMigrator +{ + private static readonly Guid[] ObsoleteBuiltInPlugins = + [ + new("4e1e54fd-6636-40ad-afdc-b3b0135feab2"), + new("cad475d3-c621-4ec7-bbfc-784e3b4723ce"), + new("ab41d601-35e0-4a73-bf0b-94509b006ab0"), + new("27d124e3-48e8-4b0a-8a5e-d5e337a88d4a") + ]; + + public static async Task Migrate(IWorkshopService workshopService, IWorkshopClient workshopClient, ILogger logger, IPluginRepository pluginRepository) + { + // If no default plugins are present (later installs), do nothing + DirectoryInfo pluginDirectory = new(Constants.PluginsFolder); + if (!pluginDirectory.Exists) + { + return true; + } + + // Load plugin info, the plugin management service isn't available yet (which is exactly what we want) + List<(PluginInfo PluginInfo, DirectoryInfo Directory)> plugins = []; + foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories()) + { + try + { + // Load the metadata + string metadataFile = Path.Combine(subDirectory.FullName, "plugin.json"); + if (File.Exists(metadataFile)) + plugins.Add((CoreJson.Deserialize(await File.ReadAllTextAsync(metadataFile))!, subDirectory)); + } + catch (Exception) + { + // ignored, who knows what old stuff people might have in their plugins folder + } + } + + if (plugins.Count == 0) + { + return true; + } + + IWorkshopService.WorkshopStatus workshopStatus = await workshopService.GetWorkshopStatus(CancellationToken.None); + if (!workshopStatus.IsReachable) + { + logger.Warning("MigrateBuiltInPlugins - Cannot migrate built-in plugins because the workshop is unreachable"); + return false; + } + + logger.Information("MigrateBuiltInPlugins - Migrating built-in plugins to workshop entries"); + IOperationResult result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, null, CancellationToken.None); + List entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).ToList() ?? []; + while (result.Data?.EntriesV2?.PageInfo is {HasNextPage: true}) + { + result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, result.Data.EntriesV2.PageInfo.EndCursor, CancellationToken.None); + if (result.Data?.EntriesV2?.Edges != null) + entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node)); + } + + logger.Information("MigrateBuiltInPlugins - Found {Count} default plugins in the workshop", entries.Count); + foreach (IGetDefaultPlugins_EntriesV2_Edges_Node entry in entries) + { + // Skip entries without plugin info or releases, shouldn't happen but theoretically possible + if (entry.PluginInfo == null || entry.LatestRelease == null) + continue; + + // Find a built-in plugin + (PluginInfo? pluginInfo, DirectoryInfo? directory) = plugins.FirstOrDefault(p => p.PluginInfo.Guid == entry.PluginInfo.PluginGuid); + if (pluginInfo == null || directory == null) + continue; + + // If the plugin is enabled, install the workshop equivalent (the built-in plugin will be removed by the install process) + PluginEntity? entity = pluginRepository.GetPluginByPluginGuid(pluginInfo.Guid); + if (entity != null && entity.IsEnabled) + { + logger.Information("MigrateBuiltInPlugins - Migrating built-in plugin {Plugin} to workshop entry {Entry}", pluginInfo.Name, entry); + await workshopService.InstallEntry(entry, entry.LatestRelease, new Progress(), CancellationToken.None); + } + + // Remove the built-in plugin, it's no longer needed + directory.Delete(true); + } + + // Remove obsolete built-in plugins + foreach (Guid obsoleteBuiltInPlugin in ObsoleteBuiltInPlugins) + { + (PluginInfo? pluginInfo, DirectoryInfo? directory) = plugins.FirstOrDefault(p => p.PluginInfo.Guid == obsoleteBuiltInPlugin); + if (pluginInfo == null || directory == null) + continue; + + directory.Delete(true); + } + + logger.Information("MigrateBuiltInPlugins - Finished migrating built-in plugins to workshop entries"); + return true; + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs index e3a2ecde4..2f1cca251 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs @@ -1,13 +1,31 @@ -using Artemis.WebClient.Workshop.Models; +using Artemis.Core; +using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; public class EntryInstallResult { - public bool IsSuccess { get; set; } - public string? Message { get; set; } - public InstalledEntry? Entry { get; set; } + /// + /// Gets a value indicating whether the installation was successful. + /// + public bool IsSuccess { get; private set; } + + /// + /// Gets a message describing the result of the installation. + /// + public string? Message { get; private set; } + + /// + /// Gets the entry that was installed, if any. + /// + public InstalledEntry? Entry { get; private set; } + + /// + /// Gets the result object returned by the installation handler, if any. + /// This'll be a , or depending on the entry type. + /// + public object? Installed { get; private set; } public static EntryInstallResult FromFailure(string? message) { @@ -18,12 +36,13 @@ public class EntryInstallResult }; } - public static EntryInstallResult FromSuccess(InstalledEntry installedEntry) + public static EntryInstallResult FromSuccess(InstalledEntry installedEntry, object? result) { return new EntryInstallResult { IsSuccess = true, - Entry = installedEntry + Entry = installedEntry, + Installed = result }; } diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs index 849b466b5..7636c5def 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -59,7 +59,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler { installedEntry.ApplyRelease(release); _workshopService.SaveInstalledEntry(installedEntry); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, layout); } // If the layout ended up being invalid yoink it out again, shoooo diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs index d597eb4e7..b4451d9cf 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs @@ -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 (installedEntry.GetReleaseDirectory(release).Exists) - return ApplyAndSave(installedEntry, release); + return ApplyAndSave(null, installedEntry, release); } else { @@ -64,7 +64,12 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler archive.ExtractToDirectory(releaseDirectory.FullName); PluginInfo pluginInfo = CoreJson.Deserialize(await File.ReadAllTextAsync(Path.Combine(releaseDirectory.FullName, "plugin.json"), cancellationToken))!; + installedEntry.SetMetadata("PluginId", pluginInfo.Guid); + // If the plugin management service isn't loaded yet (happens while migrating from built-in plugins) we're done here + if (!_pluginManagementService.LoadedPlugins) + return ApplyAndSave(null, installedEntry, release); + // If there is already a version of the plugin installed, remove it Plugin? currentVersion = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (currentVersion != null) @@ -78,13 +83,12 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler } // Load the plugin, next time during startup this will happen automatically + Plugin? plugin = null; try { - Plugin? plugin = _pluginManagementService.LoadPlugin(releaseDirectory); + plugin = _pluginManagementService.LoadPlugin(releaseDirectory); if (plugin == null) throw new ArtemisWorkshopException("Failed to load plugin, it may be incompatible"); - - installedEntry.SetMetadata("PluginId", plugin.Guid); } catch (Exception e) { @@ -102,7 +106,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler return EntryInstallResult.FromFailure(e.Message); } - return ApplyAndSave(installedEntry, release); + return ApplyAndSave(plugin, installedEntry, release); } public Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) @@ -133,10 +137,10 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler return Task.FromResult(EntryUninstallResult.FromSuccess(message)); } - private EntryInstallResult ApplyAndSave(InstalledEntry installedEntry, IRelease release) + private EntryInstallResult ApplyAndSave(Plugin? plugin, InstalledEntry installedEntry, IRelease release) { installedEntry.ApplyRelease(release); _workshopService.SaveInstalledEntry(installedEntry); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, plugin); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index d702681a4..f16aa8d36 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -52,7 +52,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler // With everything updated, remove the old profile _profileService.RemoveProfileConfiguration(existing); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, overwritten); } } @@ -66,7 +66,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler // Update the release and return the profile configuration UpdateRelease(installedEntry, release); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, imported); } public async Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql index cdcda0581..10c5502ed 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -39,4 +39,33 @@ query GetDefaultEntries($first: Int, $after: String) { } } } +} + +query GetDefaultPlugins($first: Int, $after: String) { + entriesV2( + includeDefaults: true + where: { + and: [ + { defaultEntryInfo: { entryId: { gt: 0 } } } + { entryType: {eq: PLUGIN} } + ] + } + first: $first + after: $after + ) { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + ...entrySummary + pluginInfo { + pluginGuid + } + } + } + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index f6295a9ed..861d23c49 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -124,7 +124,7 @@ public interface IWorkshopService /// /// Initializes the workshop service. /// - void Initialize(); + Task Initialize(); /// /// Represents the status of the workshop. @@ -134,6 +134,7 @@ public interface IWorkshopService public event EventHandler? OnInstalledEntrySaved; public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; + public event EventHandler? MigratingBuildInPlugins; void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 553eb5f18..68698cec2 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -1,6 +1,7 @@ using System.Net.Http.Headers; using Artemis.Core; using Artemis.Core.Services; +using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Workshop; using Artemis.Storage.Repositories.Interfaces; using Artemis.UI.Shared.Routing; @@ -10,6 +11,7 @@ using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Models; using Serilog; +using StrawberryShake; namespace Artemis.WebClient.Workshop.Services; @@ -22,15 +24,24 @@ public class WorkshopService : IWorkshopService private readonly Lazy _pluginManagementService; private readonly Lazy _profileService; private readonly EntryInstallationHandlerFactory _factory; + private readonly IPluginRepository _pluginRepository; + private readonly IWorkshopClient _workshopClient; + private readonly PluginSetting _migratedBuiltInPlugins; + + + private bool _initialized; public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository, + ISettingsService settingsService, Lazy pluginManagementService, Lazy profileService, - EntryInstallationHandlerFactory factory) + EntryInstallationHandlerFactory factory, + IPluginRepository pluginRepository, + IWorkshopClient workshopClient) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -39,6 +50,10 @@ public class WorkshopService : IWorkshopService _pluginManagementService = pluginManagementService; _profileService = profileService; _factory = factory; + _pluginRepository = pluginRepository; + _workshopClient = workshopClient; + + _migratedBuiltInPlugins = settingsService.GetSetting("Workshop.MigratedBuiltInPlugins", false); } public async Task GetEntryIcon(long entryId, CancellationToken cancellationToken) @@ -166,7 +181,7 @@ public class WorkshopService : IWorkshopService OnEntryInstalled?.Invoke(this, result.Entry); else _logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message); - + return result; } @@ -227,7 +242,7 @@ public class WorkshopService : IWorkshopService } /// - public void Initialize() + public async Task Initialize() { if (_initialized) throw new ArtemisWorkshopException("Workshop service is already initialized"); @@ -238,6 +253,7 @@ public class WorkshopService : IWorkshopService Directory.CreateDirectory(Constants.WorkshopFolder); RemoveOrphanedFiles(); + await MigrateBuiltInPlugins(); _pluginManagementService.Value.AdditionalPluginDirectories.AddRange(GetInstalledEntries() .Where(e => e.EntryType == EntryType.Plugin) @@ -259,7 +275,7 @@ public class WorkshopService : IWorkshopService { if (installedEntry.AutoUpdate == autoUpdate) return; - + installedEntry.AutoUpdate = autoUpdate; SaveInstalledEntry(installedEntry); } @@ -297,6 +313,19 @@ public class WorkshopService : IWorkshopService } } + private async Task MigrateBuiltInPlugins() + { + // If already migrated, do nothing + if (_migratedBuiltInPlugins.Value) + return; + + MigratingBuildInPlugins?.Invoke(this, EventArgs.Empty); + + bool migrated = await BuiltInPluginsMigrator.Migrate(this, _workshopClient, _logger, _pluginRepository); + _migratedBuiltInPlugins.Value = migrated; + _migratedBuiltInPlugins.Save(); + } + private void ProfileServiceOnProfileRemoved(object? sender, ProfileConfigurationEventArgs e) { InstalledEntry? entry = GetInstalledEntryByProfile(e.ProfileConfiguration); @@ -322,4 +351,6 @@ public class WorkshopService : IWorkshopService public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; + + public event EventHandler? MigratingBuildInPlugins; } \ No newline at end of file