From 3022c7df651cfa2afb7095f2462610dfe697165d Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Thu, 16 May 2024 20:40:45 +0200 Subject: [PATCH] Core - Add device provider suspension system --- .../Plugins/DeviceProviders/DeviceProvider.cs | 16 +++- src/Artemis.Core/Services/DeviceService.cs | 91 +++++++++++++++++-- .../Services/Interfaces/IDeviceService.cs | 11 +++ src/Artemis.UI.Windows/App.axaml.cs | 17 ++-- .../Artemis.UI.Windows.csproj | 1 + src/Artemis.UI.Windows/SuspensionManager.cs | 67 ++++++++++++++ .../Services/AuthenticationService.cs | 2 +- src/Directory.Packages.props | 1 + 8 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 src/Artemis.UI.Windows/SuspensionManager.cs diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index ee9f256a9..263748f0d 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using RGB.NET.Core; namespace Artemis.Core.DeviceProviders; @@ -15,7 +16,7 @@ public abstract class DeviceProvider : PluginFeature /// The RGB.NET device provider backing this Artemis device provider /// public abstract IRGBDeviceProvider RgbDeviceProvider { get; } - + /// /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. /// @@ -48,6 +49,11 @@ public abstract class DeviceProvider : PluginFeature /// public bool RemoveExcessiveLedsSupported { get; protected set; } = true; + /// + /// Gets or sets a boolean indicating whether suspending the device provider is supported + /// + public bool SuspendSupported { get; protected set; } + /// /// Loads a layout for the specified device and wraps it in an /// @@ -109,4 +115,12 @@ public abstract class DeviceProvider : PluginFeature return fileName + ".xml"; } + + /// + /// Called when the device provider is being suspended, like when the system is going to sleep. + /// Note: This will be called while the plugin is disabled. + /// + public virtual void Suspend() + { + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index 111594c4a..5d2da4db4 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; using Artemis.Core.Providers; using Artemis.Core.Services.Models; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; -using DryIoc; using RGB.NET.Core; using Serilog; @@ -21,8 +21,10 @@ internal class DeviceService : IDeviceService private readonly IDeviceRepository _deviceRepository; private readonly Lazy _renderService; private readonly Func> _getLayoutProviders; - private readonly List _enabledDevices = new(); - private readonly List _devices = new(); + private readonly List _enabledDevices = []; + private readonly List _devices = []; + private readonly List _suspendedDeviceProviders = []; + private readonly object _suspensionLock = new(); public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, @@ -69,7 +71,7 @@ internal class DeviceService : IDeviceService OnDeviceRemoved(new DeviceEventArgs(device)); } - List providerExceptions = new(); + List providerExceptions = []; void DeviceProviderOnException(object? sender, ExceptionEventArgs e) { @@ -95,7 +97,7 @@ internal class DeviceService : IDeviceService return; } - List addedDevices = new(); + List addedDevices = []; foreach (IRGBDevice rgbDevice in rgbDeviceProvider.Devices) { ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice); @@ -184,7 +186,7 @@ internal class DeviceService : IDeviceService device.ApplyLayout(null, false, false); else provider?.ApplyLayout(device, layout); - + UpdateLeds(); } catch (Exception e) @@ -241,6 +243,83 @@ internal class DeviceService : IDeviceService UpdateLeds(); } + /// + public void SuspendDeviceProviders() + { + lock (_suspensionLock) + { + _logger.Information("Suspending all device providers"); + + bool wasPaused = _renderService.Value.IsPaused; + try + { + _renderService.Value.IsPaused = true; + foreach (DeviceProvider deviceProvider in _pluginManagementService.GetFeaturesOfType().Where(d => d.SuspendSupported)) + SuspendDeviceProvider(deviceProvider); + } + finally + { + _renderService.Value.IsPaused = wasPaused; + } + } + } + + /// + public void ResumeDeviceProviders() + { + lock (_suspensionLock) + { + _logger.Information("Resuming all device providers"); + + bool wasPaused = _renderService.Value.IsPaused; + try + { + _renderService.Value.IsPaused = true; + foreach (DeviceProvider deviceProvider in _suspendedDeviceProviders.ToList()) + ResumeDeviceProvider(deviceProvider); + } + finally + { + _renderService.Value.IsPaused = wasPaused; + } + } + } + + private void SuspendDeviceProvider(DeviceProvider deviceProvider) + { + if (_suspendedDeviceProviders.Contains(deviceProvider)) + { + _logger.Warning("Device provider {DeviceProvider} is already suspended", deviceProvider.Info.Name); + return; + } + + try + { + _pluginManagementService.DisablePluginFeature(deviceProvider, false); + deviceProvider.Suspend(); + _suspendedDeviceProviders.Add(deviceProvider); + _logger.Information("Device provider {DeviceProvider} suspended", deviceProvider.Info.Name); + } + catch (Exception e) + { + _logger.Error(e, "Device provider {DeviceProvider} failed to suspend", deviceProvider.Info.Name); + } + } + + private void ResumeDeviceProvider(DeviceProvider deviceProvider) + { + try + { + _pluginManagementService.EnablePluginFeature(deviceProvider, false, true); + _suspendedDeviceProviders.Remove(deviceProvider); + _logger.Information("Device provider {DeviceProvider} resumed", deviceProvider.Info.Name); + } + catch (Exception e) + { + _logger.Error(e, "Device provider {DeviceProvider} failed to resume", deviceProvider.Info.Name); + } + } + private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice) { string deviceIdentifier = rgbDevice.GetDeviceIdentifier(); diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index 51b19d937..5c5c60ad7 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Artemis.Core.DeviceProviders; namespace Artemis.Core.Services; @@ -71,6 +72,16 @@ public interface IDeviceService : IArtemisService /// void SaveDevices(); + /// + /// Suspends all active device providers + /// + void SuspendDeviceProviders(); + + /// + /// Resumes all previously active device providers + /// + void ResumeDeviceProviders(); + /// /// Occurs when a single device was added. /// diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 66d5aac1b..593a9ea2a 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -35,10 +35,10 @@ public class App : Application } _container = ArtemisBootstrapper.Bootstrap(this, c => c.RegisterProviders()); - + Program.CreateLogger(_container); LegacyMigrationService.MigrateToSqlite(_container); - + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } @@ -47,10 +47,13 @@ public class App : Application { if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown) return; - - _applicationStateManager = new ApplicationStateManager(_container!, desktop.Args ?? Array.Empty()); + if (_container == null) + throw new InvalidOperationException("Container is null"); + + _applicationStateManager = new ApplicationStateManager(_container, desktop.Args ?? Array.Empty()); + _suspensionManager = new SuspensionManager(_container); ArtemisBootstrapper.Initialize(); - RegisterProviders(_container!); + RegisterProviders(_container); } private void RegisterProviders(IContainer container) @@ -117,8 +120,10 @@ public class App : Application } // ReSharper disable NotAccessedField.Local - private ApplicationStateManager? _applicationStateManager; + private ApplicationStateManager? _applicationStateManager; + private SuspensionManager? _suspensionManager; private Mutex? _artemisMutex; + // ReSharper restore NotAccessedField.Local } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index f74f2bd4b..0547102a2 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Artemis.UI.Windows/SuspensionManager.cs b/src/Artemis.UI.Windows/SuspensionManager.cs new file mode 100644 index 000000000..18da8db80 --- /dev/null +++ b/src/Artemis.UI.Windows/SuspensionManager.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Artemis.Core.Services; +using DryIoc; +using Microsoft.Win32; +using Serilog; + +namespace Artemis.UI.Windows; + +public class SuspensionManager +{ + private readonly ILogger _logger; + private readonly IDeviceService _deviceService; + + public SuspensionManager(IContainer container) + { + _logger = container.Resolve(); + _deviceService = container.Resolve(); + + try + { + SystemEvents.PowerModeChanged += SystemEventsOnPowerModeChanged; + SystemEvents.SessionSwitch += SystemEventsOnSessionSwitch; + } + catch (Exception e) + { + _logger.Warning(e, "Could not subscribe to system events"); + } + } + + private void SystemEventsOnPowerModeChanged(object sender, PowerModeChangedEventArgs e) + { + if (e.Mode == PowerModes.Suspend) + Task.Run(() => SetDeviceSuspension(true)); + else if (e.Mode == PowerModes.Resume) + Task.Run(() => SetDeviceSuspension(false)); + } + + private void SystemEventsOnSessionSwitch(object sender, SessionSwitchEventArgs e) + { + if (e.Reason is SessionSwitchReason.SessionLock or SessionSwitchReason.SessionLogoff) + Task.Run(() => SetDeviceSuspension(true)); + else if (e.Reason is SessionSwitchReason.SessionUnlock or SessionSwitchReason.SessionLogon) + Task.Run(() => SetDeviceSuspension(false)); + } + + private async Task SetDeviceSuspension(bool suspend) + { + try + { + if (suspend) + { + // Suspend instantly, system is going into sleep at any moment + _deviceService.SuspendDeviceProviders(); + } + else + { + await Task.Delay(TimeSpan.FromSeconds(2)); + _deviceService.ResumeDeviceProviders(); + } + } + catch (Exception e) + { + _logger.Error(e, "An error occurred while setting provider suspension to {Suspension}", suspend); + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index c8727c361..9c0dc6d7b 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -21,7 +21,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi { private const string CLIENT_ID = "artemis.desktop"; private readonly IAuthenticationRepository _authenticationRepository; - private readonly SemaphoreSlim _authLock = new(1); + private readonly SemaphoreSlim _authLock = new(1, 1); private readonly SourceList _claims; private readonly IDiscoveryCache _discoveryCache; diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e85192de6..3eb203ff3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -14,6 +14,7 @@ +