diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index a245b74e7..67b09bc8e 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; using Artemis.Core.Exceptions; using Artemis.Core.Plugins.Settings; using Artemis.Core.Services.Interfaces; @@ -44,10 +46,6 @@ namespace Artemis.Core.Ninject Kernel.Bind().ToMethod(t => { - // Ensure the data folder exists - if (!Directory.Exists(Constants.DataFolder)) - Directory.CreateDirectory(Constants.DataFolder); - try { return new LiteRepository(Constants.ConnectionString); diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index 7f77ca5f8..8a2420589 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -91,6 +91,12 @@ namespace Artemis.Core.Plugins.Modules /// public bool IsActivated { get; internal set; } + /// + /// Gets whether this module's activation was due to an override, can only be true if is + /// true + /// + public bool IsActivatedOverride { get; set; } + /// /// A list of activation requirements /// Note: if empty the module is always activated @@ -182,6 +188,7 @@ namespace Artemis.Core.Plugins.Modules if (IsActivated) return; + IsActivatedOverride = isOverride; ModuleActivated(isOverride); IsActivated = true; } @@ -191,6 +198,7 @@ namespace Artemis.Core.Plugins.Modules if (!IsActivated) return; + IsActivatedOverride = false; IsActivated = false; ModuleDeactivated(isOverride); } diff --git a/src/Artemis.Core/Services/Interfaces/IModuleService.cs b/src/Artemis.Core/Services/Interfaces/IModuleService.cs index 509c5048a..5d43bb646 100644 --- a/src/Artemis.Core/Services/Interfaces/IModuleService.cs +++ b/src/Artemis.Core/Services/Interfaces/IModuleService.cs @@ -8,11 +8,6 @@ namespace Artemis.Core.Services.Interfaces /// public interface IModuleService : IArtemisService { - /// - /// Gets whether an override is currently being applied - /// - bool ApplyingOverride { get; } - /// /// Gets the current active module override. If set, all other modules are deactivated and only the /// is active. diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index f3f5eef53..7a5560262 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -6,6 +6,8 @@ using System.Threading; using System.Threading.Tasks; using System.Timers; using Artemis.Core.Events; +using Artemis.Core.Exceptions; +using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Plugins.Modules; using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Storage.Interfaces; @@ -17,6 +19,7 @@ namespace Artemis.Core.Services { internal class ModuleService : IModuleService { + private static readonly SemaphoreSlim ActiveModuleSemaphore = new SemaphoreSlim(1, 1); private readonly ILogger _logger; private readonly IModuleRepository _moduleRepository; private readonly IPluginService _pluginService; @@ -37,7 +40,6 @@ namespace Artemis.Core.Services PopulatePriorities(); } - public bool ApplyingOverride { get; private set; } public Module ActiveModuleOverride { get; private set; } public async Task SetActiveModuleOverride(Module overrideModule) @@ -45,42 +47,43 @@ namespace Artemis.Core.Services if (ActiveModuleOverride == overrideModule) return; + if (!await ActiveModuleSemaphore.WaitAsync(TimeSpan.FromSeconds(10))) + throw new ArtemisCoreException("Timed out while acquiring active module lock"); + try { - // Not the cleanest way but locks don't work async and I cba with a mutex - while (ApplyingOverride) - await Task.Delay(50); - - ApplyingOverride = true; ActiveModuleOverride = overrideModule; // If set to null, resume regular activation if (ActiveModuleOverride == null) { - await UpdateModuleActivation(); _logger.Information("Cleared active module override"); return; } // If a module was provided, activate it and deactivate everything else var modules = _pluginService.GetPluginsOfType().ToList(); - var deactivationTasks = new List(); + var tasks = new List(); foreach (var module in modules) { if (module != ActiveModuleOverride) - deactivationTasks.Add(DeactivateModule(module, true)); + tasks.Add(DeactivateModule(module, true)); } - await Task.WhenAll(deactivationTasks); - if (!ActiveModuleOverride.IsActivated) - await ActivateModule(ActiveModuleOverride, true); + tasks.Add(ActivateModule(ActiveModuleOverride, true)); + + await Task.WhenAll(tasks); _logger.Information($"Set active module override to {ActiveModuleOverride.DisplayName}"); } finally { - ApplyingOverride = false; + ActiveModuleSemaphore.Release(); + + // With the semaphore released, trigger an update with the override was cleared + if (ActiveModuleOverride == null) + await UpdateModuleActivation(); } } @@ -89,25 +92,35 @@ namespace Artemis.Core.Services if (ActiveModuleOverride != null) return; - var stopwatch = new Stopwatch(); - stopwatch.Start(); + if (!await ActiveModuleSemaphore.WaitAsync(TimeSpan.FromSeconds(10))) + throw new ArtemisCoreException("Timed out while acquiring active module lock"); - var modules = _pluginService.GetPluginsOfType().ToList(); - var tasks = new List(); - foreach (var module in modules) + try { - var shouldBeActivated = module.EvaluateActivationRequirements(); - if (shouldBeActivated && !module.IsActivated) - tasks.Add(ActivateModule(module, false)); - else if (!shouldBeActivated && module.IsActivated) - tasks.Add(DeactivateModule(module, false)); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var modules = _pluginService.GetPluginsOfType().ToList(); + var tasks = new List(); + foreach (var module in modules) + { + var shouldBeActivated = module.EvaluateActivationRequirements(); + if (shouldBeActivated && !module.IsActivated) + tasks.Add(ActivateModule(module, false)); + else if (!shouldBeActivated && module.IsActivated) + tasks.Add(DeactivateModule(module, false)); + } + + await Task.WhenAll(tasks); + + stopwatch.Stop(); + if (stopwatch.ElapsedMilliseconds > 100 && !tasks.Any()) + _logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", modules.Count, stopwatch.Elapsed); + } + finally + { + ActiveModuleSemaphore.Release(); } - - await Task.WhenAll(tasks); - - stopwatch.Stop(); - if (stopwatch.ElapsedMilliseconds > 100) - _logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", modules.Count, stopwatch.Elapsed); } public void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority) @@ -144,20 +157,36 @@ namespace Artemis.Core.Services private async Task ActivateModule(Module module, bool isOverride) { - module.Activate(isOverride); + try + { + module.Activate(isOverride); - // If this is a profile module, activate the last active profile after module activation - if (module is ProfileModule profileModule) - await _profileService.ActivateLastProfileAnimated(profileModule); + // If this is a profile module, activate the last active profile after module activation + if (module is ProfileModule profileModule) + await _profileService.ActivateLastProfileAnimated(profileModule); + } + catch (Exception e) + { + _logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to activate module and last profile.", e), "Failed to activate module and last profile"); + throw; + } } private async Task DeactivateModule(Module module, bool isOverride) { - // If this is a profile module, activate the last active profile after module activation - if (module.IsActivated && module is ProfileModule profileModule) - await profileModule.ChangeActiveProfileAnimated(null, null); + try + { + // If this is a profile module, activate the last active profile after module activation + if (module.IsActivated && module is ProfileModule profileModule) + await profileModule.ChangeActiveProfileAnimated(null, null); - module.Deactivate(isOverride); + module.Deactivate(isOverride); + } + catch (Exception e) + { + _logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"); + throw; + } } private void PopulatePriorities() diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 1de95a0d3..5ed4072b8 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -8,10 +8,12 @@ using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Plugins.Modules; using Artemis.Core.Services.Storage.Interfaces; using Artemis.UI.Shared.Events; +using Artemis.UI.Shared.Exceptions; using Artemis.UI.Shared.PropertyInput; using Artemis.UI.Shared.Services.Interfaces; using Ninject; using Serilog; +using Stylet; namespace Artemis.UI.Shared.Services { @@ -20,10 +22,10 @@ namespace Artemis.UI.Shared.Services private readonly ILogger _logger; private readonly IProfileService _profileService; private readonly List _registeredPropertyEditors; - private TimeSpan _currentTime; - private int _pixelsPerSecond; private readonly object _selectedProfileElementLock = new object(); private readonly object _selectedProfileLock = new object(); + private TimeSpan _currentTime; + private int _pixelsPerSecond; public ProfileEditorService(IProfileService profileService, IKernel kernel, ILogger logger) { @@ -72,11 +74,20 @@ namespace Artemis.UI.Shared.Services if (SelectedProfile == profile) return; + if (profile != null && !profile.IsActivated) + throw new ArtemisSharedUIException("Cannot change the selected profile to an inactive profile"); + _logger.Verbose("ChangeSelectedProfile {profile}", profile); ChangeSelectedProfileElement(null); var profileElementEvent = new ProfileEventArgs(profile, SelectedProfile); + + // Ensure there is never a deactivated profile as the selected profile + if (SelectedProfile != null) + SelectedProfile.Deactivated -= SelectedProfileOnDeactivated; SelectedProfile = profile; + if (SelectedProfile != null) + SelectedProfile.Deactivated += SelectedProfileOnDeactivated; OnSelectedProfileChanged(profileElementEvent); UpdateProfilePreview(); @@ -309,5 +320,10 @@ namespace Artemis.UI.Shared.Services { ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty); } + + private void SelectedProfileOnDeactivated(object sender, EventArgs e) + { + Execute.PostToUIThread(() => ChangeSelectedProfile(null)); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index be17d3cbb..c337f7cd0 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; using System.Threading.Tasks; using System.Windows; -using System.Windows.Documents; using System.Windows.Markup; using System.Windows.Threading; -using Artemis.Core.Models.Profile.Conditions; +using Artemis.Core; using Artemis.Core.Ninject; using Artemis.Core.Services.Interfaces; using Artemis.UI.Ninject; @@ -37,11 +39,12 @@ namespace Artemis.UI protected override void Launch() { - StartupArguments = Args.ToList(); - var logger = Kernel.Get(); var viewManager = Kernel.Get(); + StartupArguments = Args.ToList(); + CreateDataDirectory(logger); + // Create the Artemis core try { @@ -113,6 +116,29 @@ namespace Artemis.UI e.Handled = true; } + private void CreateDataDirectory(ILogger logger) + { + // Ensure the data folder exists + if (Directory.Exists(Constants.DataFolder)) + return; + + logger.Information("Creating data directory at {dataDirectoryFolder}", Constants.DataFolder); + Directory.CreateDirectory(Constants.DataFolder); + + // During creation ensure all local users can access the data folder + // This is needed when later running Artemis as a different user or when Artemis is first run as admin + var directoryInfo = new DirectoryInfo(Constants.DataFolder); + var accessControl = directoryInfo.GetAccessControl(); + accessControl.AddAccessRule(new FileSystemAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), + FileSystemRights.FullControl, + InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, + PropagationFlags.InheritOnly, + AccessControlType.Allow) + ); + directoryInfo.SetAccessControl(accessControl); + } + private void HandleFatalException(Exception e, ILogger logger) { logger.Fatal(e, "Fatal exception during initialization, shutting down."); diff --git a/src/Artemis.UI/Screens/Module/ModuleRootViewModel.cs b/src/Artemis.UI/Screens/Module/ModuleRootViewModel.cs index 7851a2982..6125fdac1 100644 --- a/src/Artemis.UI/Screens/Module/ModuleRootViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ModuleRootViewModel.cs @@ -50,9 +50,6 @@ namespace Artemis.UI.Screens.Module private async Task AddTabsAsync() { - // Give the screen a moment to active without freezing the UI thread - await Task.Delay(400); - // Create the profile editor and module VMs if (Module is ProfileModule profileModule) { diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index c304aba1e..8ea8c0210 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -270,6 +270,9 @@ namespace Artemis.UI.Screens.ProfileEditor { Execute.PostToUIThread(async () => { + if (SelectedProfile == null) + return; + var changeTask = _profileService.ActivateProfileAnimated(SelectedProfile); _profileEditorService.ChangeSelectedProfile(null); var profile = await changeTask; @@ -279,7 +282,10 @@ namespace Artemis.UI.Screens.ProfileEditor private void ModuleOnActiveProfileChanged(object sender, EventArgs e) { - SelectedProfile = Profiles.FirstOrDefault(d => d.Id == Module.ActiveProfile.EntityId); + if (Module.ActiveProfile == null) + SelectedProfile = null; + else + SelectedProfile = Profiles.FirstOrDefault(d => d.Id == Module.ActiveProfile.EntityId); } private void LoadWorkspaceSettings()