From 3269c8732926f590cc27f1ff9fbeb6f46cd60e82 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 15 Oct 2025 17:58:15 +0200 Subject: [PATCH 1/5] Add PowerMode provider selection This PR adds settings to choose a Power Mode provider, which will detect if the system is in a paused/suspended state and avoid starting tasks while being suspended. This currently only works for Windows, where the previous inplementation is named `NET` (for .NET) and the new mode is named `Native` and using the Windows documented approach with a hidden window that listens for `WM_POWERBROADCAST` messages. The default is `Native` but can now also be disabled by choosing the `None` provider. --- .../Library/Interface/IPowerModeProvider.cs | 42 ++++++ .../RestAPI/Database/ServerSettings.cs | 21 +++ .../RestAPI/Duplicati.Library.RestAPI.csproj | 4 - Duplicati/Library/RestAPI/LiveControls.cs | 121 ++++++++++-------- .../Duplicati.Library.Snapshots.csproj | 1 + .../Library/Snapshots/PowerModeProvider.cs | 46 +++++++ .../Library/Snapshots/PowerModeUtility.cs | 54 ++++++++ .../Windows/WindowsPowerModeProvider.cs | 71 ++++++++++ .../Snapshots/Windows/WindowsShimLoader.cs | 11 +- .../Duplicati.Library.WindowsModules.csproj | 2 +- .../WindowsModules/PowerManagementModule.cs | 114 +++++++++++++++++ Duplicati/Server/Program.cs | 5 +- Duplicati/WebserverCore/Dto/SystemInfoDto.cs | 4 + .../Services/SystemInfoProvider.cs | 10 ++ 14 files changed, 444 insertions(+), 62 deletions(-) create mode 100644 Duplicati/Library/Interface/IPowerModeProvider.cs create mode 100644 Duplicati/Library/Snapshots/PowerModeProvider.cs create mode 100644 Duplicati/Library/Snapshots/PowerModeUtility.cs create mode 100644 Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs create mode 100644 Duplicati/Library/WindowsModules/PowerManagementModule.cs diff --git a/Duplicati/Library/Interface/IPowerModeProvider.cs b/Duplicati/Library/Interface/IPowerModeProvider.cs new file mode 100644 index 000000000..21e5fac13 --- /dev/null +++ b/Duplicati/Library/Interface/IPowerModeProvider.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System; + +namespace Duplicati.Library.Interface; + +/// +/// Interface for a provider of power mode events +/// +public interface IPowerModeProvider : IDisposable +{ + /// + /// Event that is triggered when the system is resuming from suspend + /// + Action? OnResume { get; set; } + + /// + /// Event that is triggered when the system is suspending + /// + Action? OnSuspend { get; set; } +} diff --git a/Duplicati/Library/RestAPI/Database/ServerSettings.cs b/Duplicati/Library/RestAPI/Database/ServerSettings.cs index 7daf18ea5..7d49b31c4 100644 --- a/Duplicati/Library/RestAPI/Database/ServerSettings.cs +++ b/Duplicati/Library/RestAPI/Database/ServerSettings.cs @@ -31,6 +31,7 @@ using Duplicati.Library.Utility; using Duplicati.Library.AutoUpdater; using Microsoft.Extensions.DependencyInjection; using Duplicati.WebserverCore.Abstractions; +using Duplicati.Library.Snapshots; #nullable enable @@ -77,6 +78,7 @@ namespace Duplicati.Server.Database public const string ADDITIONAL_REPORT_URL = "additional-report-url"; public const string BACKUP_LIST_SORT_ORDER = "backup-list-sort-order"; public const string DISABLE_API_EXTENSIONS = "disable-api-extensions"; + public const string POWER_MODE_PROVIDER = "power-mode-provider"; } private readonly Dictionary settings; @@ -155,6 +157,7 @@ namespace Duplicati.Server.Database provider?.GetRequiredService()?.SignalServerSettingsUpdated(); // If throttle options were changed, update now provider?.GetRequiredService()?.GetCurrentTask()?.UpdateThrottleSpeeds(UploadSpeedLimit, DownloadSpeedLimit); + provider?.GetRequiredService()?.UpdatePowerModeProvider(); } // In case the usage reporter is enabled or disabled, refresh now @@ -841,6 +844,24 @@ namespace Duplicati.Server.Database } } + public PowerModeProvider PowerModeProvider + { + get + { + var provider = settings[CONST.POWER_MODE_PROVIDER]; + if (Enum.TryParse(provider, true, out var parsedProvider)) + return parsedProvider; + + return PowerModeProvider.Default; + } + set + { + lock (databaseConnection.m_lock) + settings[CONST.POWER_MODE_PROVIDER] = value == PowerModeProvider.Default ? null : value.ToString(); + SaveSettings(); + } + } + } } diff --git a/Duplicati/Library/RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati/Library/RestAPI/Duplicati.Library.RestAPI.csproj index afbda2b2c..c5a360a33 100644 --- a/Duplicati/Library/RestAPI/Duplicati.Library.RestAPI.csproj +++ b/Duplicati/Library/RestAPI/Duplicati.Library.RestAPI.csproj @@ -5,10 +5,6 @@ Copyright © 2025 Team Duplicati, MIT license - - - - diff --git a/Duplicati/Library/RestAPI/LiveControls.cs b/Duplicati/Library/RestAPI/LiveControls.cs index 8cffe137a..8c00195cb 100644 --- a/Duplicati/Library/RestAPI/LiveControls.cs +++ b/Duplicati/Library/RestAPI/LiveControls.cs @@ -20,8 +20,9 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Runtime.Versioning; +using Duplicati.Library.Interface; using Duplicati.Library.IO; +using Duplicati.Library.Snapshots; using Duplicati.Server.Database; namespace Duplicati.Server @@ -131,6 +132,16 @@ namespace Duplicati.Server /// private readonly Connection m_connection; + /// + /// The power mode provider, if any + /// + private IPowerModeProvider m_powerModeProvider; + + /// + /// The current power mode provider + /// + private PowerModeProvider m_currentPowerModeProvider = PowerModeProvider.None; + /// /// Constructs a new instance of the LiveControl /// @@ -199,10 +210,30 @@ namespace Duplicati.Server } } + UpdatePowerModeProvider(); + } + + /// + /// Updates the current power mode provider, if changed + /// + public void UpdatePowerModeProvider() + { try { - if (OperatingSystem.IsWindows()) - RegisterHibernateMonitor(); + var newProvider = m_connection.ApplicationSettings.PowerModeProvider; + if (newProvider == m_currentPowerModeProvider) + return; + + if (m_powerModeProvider != null) + System.Threading.Interlocked.Exchange(ref m_powerModeProvider, null)?.Dispose(); + + m_powerModeProvider = PowerModeUtility.GetPowerModeProvider(newProvider); + m_currentPowerModeProvider = newProvider; + if (m_powerModeProvider != null) + { + m_powerModeProvider.OnResume = OnResume; + m_powerModeProvider.OnSuspend = OnSuspend; + } } catch { } } @@ -362,71 +393,55 @@ namespace Duplicati.Server public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } } /// - /// Method for calling a Win32 API + /// Method called when the power mode provider signals suspend /// - [SupportedOSPlatform("windows")] - private void RegisterHibernateMonitor() + private void OnSuspend() { - Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); + //If we are running, register as being paused due to suspending + if (this.m_state == LiveControlState.Running) + { + this.SetPauseMode(); + m_pausedForSuspend = true; + m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc); + } + else + { + if (m_waitTimeExpiration.Ticks != 0) + { + m_pausedForSuspend = true; + m_suspendMinimumPause = this.EstimatedPauseEnd; + ResetTimer(null); + } + } } /// - /// A monitor for detecting when the system hibernates or resumes + /// Method called when the power mode provider signals resume /// - /// Unused sender parameter - /// The event information - [SupportedOSPlatform("windows")] - private void SystemEvents_PowerModeChanged(object sender, object _e) + private void OnResume() { - Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs; - if (e == null) - return; - - if (e.Mode == Microsoft.Win32.PowerModes.Suspend) + //If we have been been paused due to suspending, we un-pause now + if (m_pausedForSuspend) { - //If we are running, register as being paused due to suspending - if (this.m_state == LiveControlState.Running) + long delayTicks = (m_suspendMinimumPause - DateTime.UtcNow).Ticks; + + var appset = m_connection.ApplicationSettings; + if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") + try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } + catch { } + + if (delayTicks > 0) { - this.SetPauseMode(); - m_pausedForSuspend = true; - m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc); + this.Pause(TimeSpan.FromTicks(delayTicks), true); } else { - if (m_waitTimeExpiration.Ticks != 0) - { - m_pausedForSuspend = true; - m_suspendMinimumPause = this.EstimatedPauseEnd; - ResetTimer(null); - } - + this.Resume(); } } - else if (e.Mode == Microsoft.Win32.PowerModes.Resume) - { - //If we have been been paused due to suspending, we un-pause now - if (m_pausedForSuspend) - { - long delayTicks = (m_suspendMinimumPause - DateTime.UtcNow).Ticks; - var appset = m_connection.ApplicationSettings; - if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") - try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } - catch { } - - if (delayTicks > 0) - { - this.Pause(TimeSpan.FromTicks(delayTicks), true); - } - else - { - this.Resume(); - } - } - - m_pausedForSuspend = false; - m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc); - } + m_pausedForSuspend = false; + m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc); } } diff --git a/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj b/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj index 3cad70355..975244589 100644 --- a/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj +++ b/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj @@ -8,6 +8,7 @@ + diff --git a/Duplicati/Library/Snapshots/PowerModeProvider.cs b/Duplicati/Library/Snapshots/PowerModeProvider.cs new file mode 100644 index 000000000..427ff2ae6 --- /dev/null +++ b/Duplicati/Library/Snapshots/PowerModeProvider.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace Duplicati.Library.Snapshots; + +/// +/// The power mode providers that are supported +/// +public enum PowerModeProvider +{ + /// + /// The default power mode provider for the system + /// + Default, + /// + /// No power mode provider (ignores events) + /// + None, + /// + /// .NET power mode provider + /// + Net, + /// + /// Native based power mode provider + /// + Native +} + diff --git a/Duplicati/Library/Snapshots/PowerModeUtility.cs b/Duplicati/Library/Snapshots/PowerModeUtility.cs new file mode 100644 index 000000000..bb7404e74 --- /dev/null +++ b/Duplicati/Library/Snapshots/PowerModeUtility.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System; +using Duplicati.Library.Interface; + +namespace Duplicati.Library.Snapshots; + +/// +/// Support class for managing power mode providers. +/// +public static class PowerModeUtility +{ + /// + /// Loads and returns a power mode provider + /// + /// The power mode provider + /// The for the power mode provider + public static IPowerModeProvider? GetPowerModeProvider(PowerModeProvider powerModeProvider) + { + if (OperatingSystem.IsWindows()) + return powerModeProvider switch + { + PowerModeProvider.Net => + new Windows.WindowsPowerModeProvider(), + PowerModeProvider.Native or PowerModeProvider.Default => + Windows.WindowsShimLoader.NewPowerModeProvider(), + _ => null + }; + + return null; + } +} + diff --git a/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs b/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs new file mode 100644 index 000000000..5b612ce81 --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Runtime.Versioning; +using Duplicati.Library.Interface; + +namespace Duplicati.Library.Snapshots.Windows; + +/// +/// Implementation of powermode handler for Windows +/// +[SupportedOSPlatform("windows")] +public class WindowsPowerModeProvider : IPowerModeProvider +{ + /// + public Action OnResume { get; set; } + /// + public Action OnSuspend { get; set; } + + /// + /// Constructs a new power mode provider + /// + public WindowsPowerModeProvider() + { + Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); + } + + /// + /// Handles the power mode events + /// + /// The event sender + /// The event args + private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e) + { + switch (e.Mode) + { + case Microsoft.Win32.PowerModes.Suspend: + OnSuspend?.Invoke(); + break; + case Microsoft.Win32.PowerModes.Resume: + OnResume?.Invoke(); + break; + } + } + + /// + public void Dispose() + { + Microsoft.Win32.SystemEvents.PowerModeChanged -= new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); + } + +} diff --git a/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs b/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs index af633b9ea..64e52c4ba 100644 --- a/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs +++ b/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs @@ -47,7 +47,7 @@ public static class WindowsShimLoader /// Cache of types already loaded /// private static readonly Dictionary _loadedTypes = new Dictionary(); - + /// /// Cached reference to the assembly we are loading from /// @@ -100,7 +100,7 @@ public static class WindowsShimLoader var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); return path is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(path); } - } + } /// /// Loads a type using reflection @@ -154,6 +154,13 @@ public static class WindowsShimLoader public static IDisposable NewSeBackupPrivilegeScope() => LoadWithReflection("SeBackupPrivilegeScope"); + /// + /// Creates a new PowerModeProvider that can notify of suspend/resume events + /// + /// A new PowerModeProvider + public static IPowerModeProvider NewPowerModeProvider() + => LoadWithReflection("PowerManagementModule"); + /// /// Creates a new BackupDataStream for reading data with BackupRead /// diff --git a/Duplicati/Library/WindowsModules/Duplicati.Library.WindowsModules.csproj b/Duplicati/Library/WindowsModules/Duplicati.Library.WindowsModules.csproj index 4d868ff5a..b63a54699 100644 --- a/Duplicati/Library/WindowsModules/Duplicati.Library.WindowsModules.csproj +++ b/Duplicati/Library/WindowsModules/Duplicati.Library.WindowsModules.csproj @@ -11,11 +11,11 @@ + - diff --git a/Duplicati/Library/WindowsModules/PowerManagementModule.cs b/Duplicati/Library/WindowsModules/PowerManagementModule.cs new file mode 100644 index 000000000..b2ba649f4 --- /dev/null +++ b/Duplicati/Library/WindowsModules/PowerManagementModule.cs @@ -0,0 +1,114 @@ + +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System; +using System.Threading; +using Duplicati.Library.Interface; +using Vanara.PInvoke; +using static Vanara.PInvoke.User32; + +namespace Duplicati.Library.WindowsModules; + +/// +/// Implementation of a power mode provider using a hidden window to receive power broadcast messages +/// +class PowerManagementModule : IPowerModeProvider +{ + /// + /// The message loop thread + /// + private readonly Thread _messageThread; + /// + /// Event to signal that initialization is complete + /// + private readonly ManualResetEvent _init = new(false); + /// + /// The window handle for the hidden window + /// + private HWND _hwnd; + + /// + /// Event that is triggered when the system is resuming from suspend + /// + public Action? OnResume { get; set; } + /// + /// Event that is triggered when the system is suspending + /// + public Action? OnSuspend { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public PowerManagementModule() + { + _messageThread = new Thread(MessageLoop) { IsBackground = true }; + _messageThread.Start(); + _init.WaitOne(); + } + + /// + /// The message loop for the hidden window + /// + private void MessageLoop() + { + _hwnd = CreateWindowEx( + 0, "STATIC", "PowerMonitorWnd", + WindowStyles.WS_OVERLAPPED, + 0, 0, 0, 0, HWND.NULL, HMENU.NULL, HINSTANCE.NULL, IntPtr.Zero); + + _init.Set(); + + MSG msg; + while (GetMessage(out msg, HWND.NULL, 0, 0) > 0) + { + if (msg.message == (uint)WindowMessage.WM_POWERBROADCAST) + { + switch ((PowerBroadcastType)msg.wParam) + { + case PowerBroadcastType.PBT_APMSUSPEND: + OnSuspend?.Invoke(); + break; + case PowerBroadcastType.PBT_APMRESUMEAUTOMATIC: + case PowerBroadcastType.PBT_APMRESUMESUSPEND: + OnResume?.Invoke(); + break; + } + } + TranslateMessage(in msg); + DispatchMessage(in msg); + } + } + + /// + /// Disposes the power mode provider and stops listening for events + /// + public void Dispose() + { + if (_hwnd != HWND.NULL) + { + PostMessage(_hwnd, (uint)WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + _messageThread.Join(); + } + } +} \ No newline at end of file diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 20e6cdd8c..858dc7b55 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -227,7 +227,7 @@ namespace Duplicati.Server SetPurgeTempFilesTimer(connection, commandlineOptions); - LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, connection, eventPollNotify, e); }; + LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, connection, eventPollNotify, scheduler, e); }; if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, PING_PONG_KEEPALIVE_OPTION)) { @@ -848,7 +848,7 @@ namespace Duplicati.Server /// This event handler updates the trayicon menu with the current state of the runner. /// /// - private static void LiveControl_StateChanged(IQueueRunnerService queueRunnerService, Connection connection, EventPollNotify eventPollNotify, LiveControls.LiveControlEvent e) + private static void LiveControl_StateChanged(IQueueRunnerService queueRunnerService, Connection connection, EventPollNotify eventPollNotify, ISchedulerService schedulerService, LiveControls.LiveControlEvent e) { var appSettings = connection.ApplicationSettings; switch (e.State) @@ -864,6 +864,7 @@ namespace Duplicati.Server { queueRunnerService.Resume(); queueRunnerService.GetCurrentTask()?.Resume(); + schedulerService?.Reschedule(); appSettings.PausedUntil = null; break; } diff --git a/Duplicati/WebserverCore/Dto/SystemInfoDto.cs b/Duplicati/WebserverCore/Dto/SystemInfoDto.cs index 2f8f4709e..975ca6696 100644 --- a/Duplicati/WebserverCore/Dto/SystemInfoDto.cs +++ b/Duplicati/WebserverCore/Dto/SystemInfoDto.cs @@ -227,6 +227,10 @@ public sealed record SystemInfoDto /// The new default OAuth URL for v2 authentication. /// public required string DefaultOAuthURLv2 { get; init; } + /// + /// The supported power mode providers + /// + public required IEnumerable PowerModeProviders { get; init; } /// /// Represents a timezone. diff --git a/Duplicati/WebserverCore/Services/SystemInfoProvider.cs b/Duplicati/WebserverCore/Services/SystemInfoProvider.cs index 4505d03b9..d955d7208 100644 --- a/Duplicati/WebserverCore/Services/SystemInfoProvider.cs +++ b/Duplicati/WebserverCore/Services/SystemInfoProvider.cs @@ -21,6 +21,7 @@ using System.Globalization; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Localization; +using Duplicati.Library.Snapshots; using Duplicati.Library.Utility.Options; using Duplicati.Server; using Duplicati.Server.Database; @@ -228,6 +229,11 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec /// The timezones available on the system /// public required IEnumerable TimeZones { get; init; } + + /// + /// The power mode providers supported + /// + public required string[] PowerModeProviders { get; init; } } /// @@ -266,6 +272,9 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec SecretProviderModules = Server.Serializable.ServerSettings.SecretProviderModules, UsingAlternateUpdateURLs = AutoUpdateSettings.UsesAlternateURLs, LogLevels = Enum.GetNames(typeof(Library.Logging.LogMessageType)), + PowerModeProviders = OperatingSystem.IsWindows() + ? [string.Empty, PowerModeProvider.None.ToString(), PowerModeProvider.Net.ToString(), PowerModeProvider.Native.ToString()] + : [string.Empty, PowerModeProvider.None.ToString()], SpecialFolders = SpecialFolders.Nodes.Select(n => new Dto.SystemInfoDto.SpecialFolderDto { ID = n.id, Path = n.resolvedpath }).ToArray(), SupportedLocales = Library.Localization.LocalizationService.SupportedCultures .Select(x => new Dto.SystemInfoDto.LocaleDto @@ -363,6 +372,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec TimeZones = systeminfo.TimeZones, DefaultOAuthURL = AuthIdOptionsHelper.DUPLICATI_OAUTH_SERVICE, DefaultOAuthURLv2 = AuthIdOptionsHelper.DUPLICATI_OAUTH_SERVICE_NEW, + PowerModeProviders = systeminfo.PowerModeProviders, }; } } From 3f4eb39a1c6cbc2394ae1912488364fa8ed8e0ec Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 18 Oct 2025 12:36:08 +0200 Subject: [PATCH 2/5] Re-implemented the powermode detection --- .../Windows/WindowsPowerModeProvider.cs | 6 +- .../WindowsModules/PowerManagementModule.cs | 493 ++++++++++++++++-- 2 files changed, 451 insertions(+), 48 deletions(-) diff --git a/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs b/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs index 5b612ce81..603a5c9fe 100644 --- a/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs +++ b/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs @@ -19,6 +19,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +#nullable enable + using System; using System.Runtime.Versioning; using Duplicati.Library.Interface; @@ -32,9 +34,9 @@ namespace Duplicati.Library.Snapshots.Windows; public class WindowsPowerModeProvider : IPowerModeProvider { /// - public Action OnResume { get; set; } + public Action? OnResume { get; set; } /// - public Action OnSuspend { get; set; } + public Action? OnSuspend { get; set; } /// /// Constructs a new power mode provider diff --git a/Duplicati/Library/WindowsModules/PowerManagementModule.cs b/Duplicati/Library/WindowsModules/PowerManagementModule.cs index b2ba649f4..3bec9ec85 100644 --- a/Duplicati/Library/WindowsModules/PowerManagementModule.cs +++ b/Duplicati/Library/WindowsModules/PowerManagementModule.cs @@ -1,4 +1,3 @@ - // Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // @@ -23,92 +22,494 @@ #nullable enable using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; using Duplicati.Library.Interface; -using Vanara.PInvoke; -using static Vanara.PInvoke.User32; namespace Duplicati.Library.WindowsModules; /// -/// Implementation of a power mode provider using a hidden window to receive power broadcast messages +/// Provides power management functionality for Windows, monitoring system suspend and resume events. +/// Implements IPowerModeProvider to notify about power state changes. /// -class PowerManagementModule : IPowerModeProvider +[SupportedOSPlatform("windows")] +public sealed class PowerManagementModule : IPowerModeProvider, IDisposable { /// - /// The message loop thread + /// The background thread that runs the message loop for handling Windows messages. /// - private readonly Thread _messageThread; - /// - /// Event to signal that initialization is complete - /// - private readonly ManualResetEvent _init = new(false); - /// - /// The window handle for the hidden window - /// - private HWND _hwnd; + private readonly Thread _thread; /// - /// Event that is triggered when the system is resuming from suspend + /// Manual reset event used to signal when the message loop initialization is complete. + /// + private readonly ManualResetEvent _init = new(false); + + /// + /// Handle to the hidden window used for receiving power broadcast messages. + /// + private IntPtr _hwnd = IntPtr.Zero; + + /// + /// Reference to the window procedure delegate to prevent garbage collection. + /// + private WndProc? _wndProc; // keep delegate alive + + /// + /// Handle to the power setting notification registration. + /// + private IntPtr _powerNotifyHandle = IntPtr.Zero; + + /// + /// Gets or sets the action to invoke when the system resumes from suspend. /// public Action? OnResume { get; set; } + /// - /// Event that is triggered when the system is suspending + /// Gets or sets the action to invoke when the system is about to suspend. /// public Action? OnSuspend { get; set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the PowerManagementModule class with default settings. + /// Required for reflection-based loading. /// - public PowerManagementModule() + public PowerManagementModule() : this(null) { - _messageThread = new Thread(MessageLoop) { IsBackground = true }; - _messageThread.Start(); + } + + /// + /// Initializes a new instance of the PowerManagementModule class. + /// + /// Optional GUID for a specific power setting to subscribe to, or null to skip subscription. + public PowerManagementModule(Guid? powerSettingToSubscribe = null) + { + _thread = new Thread(() => MessageLoop(powerSettingToSubscribe)) { IsBackground = true }; + _thread.Start(); _init.WaitOne(); } /// - /// The message loop for the hidden window + /// Runs the message loop for handling Windows messages, including power broadcast events. + /// Creates a hidden window and optionally subscribes to power setting notifications. /// - private void MessageLoop() + /// Optional GUID for power setting subscription. + private void MessageLoop(Guid? subscribeGuid) { + _wndProc = WndProcImpl; + + var wc = new WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(), + lpfnWndProc = _wndProc, + lpszClassName = "PowerMonitorWndClass", + hInstance = GetModuleHandle(null) + }; + var atom = RegisterClassEx(ref wc); + if (atom == 0) + { + _init.Set(); + return; + } + + // Hidden, message-only window _hwnd = CreateWindowEx( - 0, "STATIC", "PowerMonitorWnd", - WindowStyles.WS_OVERLAPPED, - 0, 0, 0, 0, HWND.NULL, HMENU.NULL, HINSTANCE.NULL, IntPtr.Zero); + 0, + wc.lpszClassName, + "PowerMonitorWnd", + 0, + 0, 0, 0, 0, + HWND_MESSAGE, + IntPtr.Zero, + wc.hInstance, + IntPtr.Zero); + + if (_hwnd == IntPtr.Zero) + { + _init.Set(); + return; + } + + if (subscribeGuid.HasValue) + { + var guid = subscribeGuid.Value; + _powerNotifyHandle = RegisterPowerSettingNotification(_hwnd, ref guid, DEVICE_NOTIFY_WINDOW_HANDLE); + } _init.Set(); MSG msg; - while (GetMessage(out msg, HWND.NULL, 0, 0) > 0) + while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0) { - if (msg.message == (uint)WindowMessage.WM_POWERBROADCAST) - { - switch ((PowerBroadcastType)msg.wParam) - { - case PowerBroadcastType.PBT_APMSUSPEND: - OnSuspend?.Invoke(); - break; - case PowerBroadcastType.PBT_APMRESUMEAUTOMATIC: - case PowerBroadcastType.PBT_APMRESUMESUSPEND: - OnResume?.Invoke(); - break; - } - } - TranslateMessage(in msg); - DispatchMessage(in msg); + TranslateMessage(ref msg); + DispatchMessage(ref msg); + } + + if (_powerNotifyHandle != IntPtr.Zero) + { + UnregisterPowerSettingNotification(_powerNotifyHandle); + _powerNotifyHandle = IntPtr.Zero; } } /// - /// Disposes the power mode provider and stops listening for events + /// Window procedure implementation that handles power broadcast messages. + /// Processes suspend and resume events, invoking the appropriate actions. + /// + /// Handle to the window. + /// Message identifier. + /// Additional message information. + /// Additional message information. + /// Result of message processing. + private IntPtr WndProcImpl(IntPtr hwnd, uint msg, UIntPtr wParam, IntPtr lParam) + { + if (msg == WM_DESTROY) + { + PostQuitMessage(0); + return IntPtr.Zero; + } + + if (msg == WM_POWERBROADCAST) + { + var evt = (uint)wParam.ToUInt64(); + switch (evt) + { + case PBT_APMSUSPEND: + OnSuspend?.Invoke(); + return new IntPtr(1); // processed + + case PBT_APMRESUMEAUTOMATIC: + case PBT_APMRESUMESUSPEND: + OnResume?.Invoke(); + return new IntPtr(1); + + case PBT_POWERSETTINGCHANGE: + // Ignored by default; can be extended if needed. + return new IntPtr(1); + } + } + return DefWindowProc(hwnd, msg, wParam, lParam); + } + + /// + /// Disposes of the PowerManagementModule, cleaning up resources and stopping the message loop. /// public void Dispose() { - if (_hwnd != HWND.NULL) + if (_hwnd != IntPtr.Zero) { - PostMessage(_hwnd, (uint)WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); - _messageThread.Join(); + PostMessage(_hwnd, WM_CLOSE, UIntPtr.Zero, IntPtr.Zero); + _thread.Join(); + _hwnd = IntPtr.Zero; } + + if (_powerNotifyHandle != IntPtr.Zero) + { + UnregisterPowerSettingNotification(_powerNotifyHandle); + _powerNotifyHandle = IntPtr.Zero; + } + + _init.Dispose(); } + + // Interop + + /// + /// Windows message for power broadcast events. + /// + private const uint WM_POWERBROADCAST = 0x0218; + + /// + /// Windows message for window close. + /// + private const uint WM_CLOSE = 0x0010; + + /// + /// Windows message for window destruction. + /// + private const uint WM_DESTROY = 0x0002; + + /// + /// Power broadcast event for system suspend. + /// + private const uint PBT_APMSUSPEND = 0x0004; + + /// + /// Power broadcast event for automatic resume from suspend. + /// + private const uint PBT_APMRESUMEAUTOMATIC = 0x0012; + + /// + /// Power broadcast event for resume from suspend. + /// + private const uint PBT_APMRESUMESUSPEND = 0x0007; + + /// + /// Power broadcast event for power setting change. + /// + private const uint PBT_POWERSETTINGCHANGE = 0x8013; + + /// + /// Flag for registering power setting notification with a window handle. + /// + private const uint DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000; + + /// + /// Handle to the message-only window. + /// + private static readonly IntPtr HWND_MESSAGE = new IntPtr(-3); + + /// + /// Delegate for the window procedure function that processes window messages. + /// + /// Handle to the window. + /// Message identifier. + /// Additional message information. + /// Additional message information. + /// Result of message processing. + private delegate IntPtr WndProc(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam); + + /// + /// Represents the window class structure used for registering a window class. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WNDCLASSEX + { + /// + /// The size, in bytes, of this structure. + /// + public uint cbSize; + + /// + /// The class style(s). + /// + public uint style; + + /// + /// A pointer to the window procedure. + /// + public WndProc lpfnWndProc; + + /// + /// The number of extra bytes to allocate following the window-class structure. + /// + public int cbClsExtra; + + /// + /// The number of extra bytes to allocate following the window instance. + /// + public int cbWndExtra; + + /// + /// A handle to the instance that contains the window procedure for the class. + /// + public IntPtr hInstance; + + /// + /// A handle to the class icon. + /// + public IntPtr hIcon; + + /// + /// A handle to the class cursor. + /// + public IntPtr hCursor; + + /// + /// A handle to the class background brush. + /// + public IntPtr hbrBackground; + + /// + /// Pointer to a null-terminated character string that specifies the resource name of the class menu. + /// + [MarshalAs(UnmanagedType.LPWStr)] public string? lpszMenuName; + + /// + /// A pointer to a null-terminated string or is an atom. + /// + [MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName; + + /// + /// A handle to a small icon that is associated with the window class. + /// + public IntPtr hIconSm; + } + + /// + /// Represents a point with x and y coordinates. + /// + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + /// + /// The x-coordinate of the point. + /// + public int x; + + /// + /// The y-coordinate of the point. + /// + public int y; + } + + /// + /// Represents a Windows message structure. + /// + [StructLayout(LayoutKind.Sequential)] + private struct MSG + { + /// + /// A handle to the window whose window procedure receives the message. + /// + public IntPtr hwnd; + + /// + /// The message identifier. + /// + public uint message; + + /// + /// Additional message information. + /// + public UIntPtr wParam; + + /// + /// Additional message information. + /// + public IntPtr lParam; + + /// + /// The time at which the message was posted. + /// + public uint time; + + /// + /// The cursor position, in screen coordinates, when the message was posted. + /// + public POINT pt; + } + + /// + /// Registers a window class for subsequent use in calls to the CreateWindowEx function. + /// + /// Pointer to a WNDCLASSEX structure containing the class information. + /// If the function succeeds, the return value is a class atom that uniquely identifies the class being registered. + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); + + /// + /// Creates an overlapped, pop-up, or child window with an extended window style. + /// + /// The extended window style of the window being created. + /// A null-terminated string or a class atom created by a previous call to RegisterClassEx. + /// The window name. + /// The style of the window being created. + /// The initial horizontal position of the window. + /// The initial vertical position of the window. + /// The width, in device units, of the window. + /// The height, in device units, of the window. + /// A handle to the parent or owner window of the window being created. + /// A handle to a menu, or specifies a child-window identifier. + /// A handle to the instance of the module to be associated with the window. + /// Pointer to a value to be passed to the window through the CREATESTRUCT structure. + /// If the function succeeds, the return value is a handle to the new window. + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + int dwStyle, + int X, + int Y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + /// + /// Retrieves a module handle for the specified module. + /// + /// The name of the loaded module (either a .dll or .exe file). + /// If the function succeeds, the return value is a handle to the specified module. + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr GetModuleHandle(string? lpModuleName); + + /// + /// Retrieves a message from the calling thread's message queue. + /// + /// Pointer to an MSG structure that receives message information. + /// Handle to the window whose messages are to be retrieved. + /// The integer value of the lowest message value to be retrieved. + /// The integer value of the highest message value to be retrieved. + /// If the function retrieves a message other than WM_QUIT, the return value is nonzero. + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + /// + /// Translates virtual-key messages into character messages. + /// + /// Pointer to an MSG structure that contains message information retrieved from GetMessage. + /// If the message is translated, the return value is nonzero. + [DllImport("user32.dll")] + private static extern bool TranslateMessage([In] ref MSG lpMsg); + + /// + /// Dispatches a message to a window procedure. + /// + /// Pointer to an MSG structure that contains the message. + /// The return value specifies the value returned by the window procedure. + [DllImport("user32.dll")] + private static extern IntPtr DispatchMessage([In] ref MSG lpMsg); + + /// + /// Calls the default window procedure to provide default processing for any window messages that an application does not process. + /// + /// Handle to the window procedure that received the message. + /// The message. + /// Additional message information. + /// Additional message information. + /// The return value is the result of the message processing and depends on the message. + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, UIntPtr wParam, IntPtr lParam); + + /// + /// Places a message in the message queue associated with the thread that created the specified window. + /// + /// Handle to the window whose window procedure is to receive the message. + /// The message to be posted. + /// Additional message-specific information. + /// Additional message-specific information. + /// If the function succeeds, the return value is nonzero. + [DllImport("user32.dll")] + private static extern bool PostMessage(IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam); + + /// + /// Indicates to the system that a thread has made a request to terminate. + /// + /// The application exit code. + [DllImport("user32.dll")] + private static extern void PostQuitMessage(int nExitCode); + + /// + /// Registers the application to receive power setting notifications for the specified power setting event. + /// + /// Handle to the window or service that will receive the notifications. + /// The GUID of the power setting for which notifications are to be sent. + /// Flags that specify the recipient and the type of notifications to send. + /// If the function succeeds, the return value is a handle to the registration. + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr RegisterPowerSettingNotification(IntPtr hRecipient, ref Guid PowerSettingGuid, uint Flags); + + /// + /// Unregisters the power setting notification. + /// + /// Handle to the registration returned by RegisterPowerSettingNotification. + /// If the function succeeds, the return value is nonzero. + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UnregisterPowerSettingNotification(IntPtr Handle); } \ No newline at end of file From 379f05a56869a9f85c87b41e1be44155424ca105 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 18 Oct 2025 13:51:16 +0200 Subject: [PATCH 3/5] New attempt, using powerprof callbacks --- .../WindowsModules/PowerManagementModule.cs | 490 +++--------------- 1 file changed, 77 insertions(+), 413 deletions(-) diff --git a/Duplicati/Library/WindowsModules/PowerManagementModule.cs b/Duplicati/Library/WindowsModules/PowerManagementModule.cs index 3bec9ec85..f87559225 100644 --- a/Duplicati/Library/WindowsModules/PowerManagementModule.cs +++ b/Duplicati/Library/WindowsModules/PowerManagementModule.cs @@ -24,492 +24,156 @@ using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Threading; using Duplicati.Library.Interface; namespace Duplicati.Library.WindowsModules; /// -/// Provides power management functionality for Windows, monitoring system suspend and resume events. -/// Implements IPowerModeProvider to notify about power state changes. +/// Provides power management functionality for Windows using the powrprof callback API. +/// Eliminates the hidden window by registering a suspend/resume callback (Windows 8+). /// [SupportedOSPlatform("windows")] public sealed class PowerManagementModule : IPowerModeProvider, IDisposable { /// - /// The background thread that runs the message loop for handling Windows messages. + /// Registration handle returned from PowerRegisterSuspendResumeNotification. /// - private readonly Thread _thread; + private IntPtr _registrationHandle = IntPtr.Zero; /// - /// Manual reset event used to signal when the message loop initialization is complete. + /// Keep a reference to the delegate to prevent it from being garbage collected. /// - private readonly ManualResetEvent _init = new(false); + private DEVICE_NOTIFY_CALLBACK_ROUTINE? _callbackRef; - /// - /// Handle to the hidden window used for receiving power broadcast messages. - /// - private IntPtr _hwnd = IntPtr.Zero; - - /// - /// Reference to the window procedure delegate to prevent garbage collection. - /// - private WndProc? _wndProc; // keep delegate alive - - /// - /// Handle to the power setting notification registration. - /// - private IntPtr _powerNotifyHandle = IntPtr.Zero; - - /// - /// Gets or sets the action to invoke when the system resumes from suspend. - /// + /// public Action? OnResume { get; set; } - /// - /// Gets or sets the action to invoke when the system is about to suspend. - /// + /// public Action? OnSuspend { get; set; } /// - /// Initializes a new instance of the PowerManagementModule class with default settings. - /// Required for reflection-based loading. + /// Initializes a new instance. Required for reflection-based loading. /// public PowerManagementModule() : this(null) { } /// - /// Initializes a new instance of the PowerManagementModule class. + /// Initializes a new instance. The parameter is ignored in this implementation. /// - /// Optional GUID for a specific power setting to subscribe to, or null to skip subscription. - public PowerManagementModule(Guid? powerSettingToSubscribe = null) + /// Unused. Present for compatibility with previous constructor. + public PowerManagementModule(Guid? _) { - _thread = new Thread(() => MessageLoop(powerSettingToSubscribe)) { IsBackground = true }; - _thread.Start(); - _init.WaitOne(); + RegisterSuspendResumeCallback(); } /// - /// Runs the message loop for handling Windows messages, including power broadcast events. - /// Creates a hidden window and optionally subscribes to power setting notifications. + /// Registers the suspend/resume callback using powrprof (Windows 8+). /// - /// Optional GUID for power setting subscription. - private void MessageLoop(Guid? subscribeGuid) + private void RegisterSuspendResumeCallback() { - _wndProc = WndProcImpl; - - var wc = new WNDCLASSEX + _callbackRef = new DEVICE_NOTIFY_CALLBACK_ROUTINE(SuspendResumeCallback); + var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { - cbSize = (uint)Marshal.SizeOf(), - lpfnWndProc = _wndProc, - lpszClassName = "PowerMonitorWndClass", - hInstance = GetModuleHandle(null) + Callback = _callbackRef, + Context = IntPtr.Zero }; - var atom = RegisterClassEx(ref wc); - if (atom == 0) + + // DEVICE_NOTIFY_CALLBACK delivers notifications via the provided delegate. + uint status = PowerRegisterSuspendResumeNotification(DEVICE_NOTIFY_CALLBACK, ref parameters, out _registrationHandle); + + // If registration fails, we keep a no-op provider (no window fallback by design). + // STATUS_SUCCESS is 0. + if (status != STATUS_SUCCESS) { - _init.Set(); - return; - } - - // Hidden, message-only window - _hwnd = CreateWindowEx( - 0, - wc.lpszClassName, - "PowerMonitorWnd", - 0, - 0, 0, 0, 0, - HWND_MESSAGE, - IntPtr.Zero, - wc.hInstance, - IntPtr.Zero); - - if (_hwnd == IntPtr.Zero) - { - _init.Set(); - return; - } - - if (subscribeGuid.HasValue) - { - var guid = subscribeGuid.Value; - _powerNotifyHandle = RegisterPowerSettingNotification(_hwnd, ref guid, DEVICE_NOTIFY_WINDOW_HANDLE); - } - - _init.Set(); - - MSG msg; - while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0) - { - TranslateMessage(ref msg); - DispatchMessage(ref msg); - } - - if (_powerNotifyHandle != IntPtr.Zero) - { - UnregisterPowerSettingNotification(_powerNotifyHandle); - _powerNotifyHandle = IntPtr.Zero; + _registrationHandle = IntPtr.Zero; } } /// - /// Window procedure implementation that handles power broadcast messages. - /// Processes suspend and resume events, invoking the appropriate actions. + /// Callback invoked by the system for suspend/resume notifications. /// - /// Handle to the window. - /// Message identifier. - /// Additional message information. - /// Additional message information. - /// Result of message processing. - private IntPtr WndProcImpl(IntPtr hwnd, uint msg, UIntPtr wParam, IntPtr lParam) + /// User-provided context (unused). + /// Power event type (e.g., PBT_APMSUSPEND, PBT_APMRESUMEAUTOMATIC). + /// Additional info (unused). + /// STATUS_SUCCESS (0) on success. + private static uint STATUS_SUCCESS => 0; + private uint SuspendResumeCallback(IntPtr context, uint type, IntPtr setting) { - if (msg == WM_DESTROY) + switch (type) { - PostQuitMessage(0); - return IntPtr.Zero; + case PBT_APMSUSPEND: + OnSuspend?.Invoke(); + break; + + case PBT_APMRESUMEAUTOMATIC: + case PBT_APMRESUMESUSPEND: + OnResume?.Invoke(); + break; } - if (msg == WM_POWERBROADCAST) - { - var evt = (uint)wParam.ToUInt64(); - switch (evt) - { - case PBT_APMSUSPEND: - OnSuspend?.Invoke(); - return new IntPtr(1); // processed - - case PBT_APMRESUMEAUTOMATIC: - case PBT_APMRESUMESUSPEND: - OnResume?.Invoke(); - return new IntPtr(1); - - case PBT_POWERSETTINGCHANGE: - // Ignored by default; can be extended if needed. - return new IntPtr(1); - } - } - return DefWindowProc(hwnd, msg, wParam, lParam); + return STATUS_SUCCESS; } - /// - /// Disposes of the PowerManagementModule, cleaning up resources and stopping the message loop. - /// + /// public void Dispose() { - if (_hwnd != IntPtr.Zero) + if (_registrationHandle != IntPtr.Zero) { - PostMessage(_hwnd, WM_CLOSE, UIntPtr.Zero, IntPtr.Zero); - _thread.Join(); - _hwnd = IntPtr.Zero; + PowerUnregisterSuspendResumeNotification(_registrationHandle); + _registrationHandle = IntPtr.Zero; } - if (_powerNotifyHandle != IntPtr.Zero) - { - UnregisterPowerSettingNotification(_powerNotifyHandle); - _powerNotifyHandle = IntPtr.Zero; - } - - _init.Dispose(); + _callbackRef = null; } // Interop - /// - /// Windows message for power broadcast events. - /// - private const uint WM_POWERBROADCAST = 0x0218; - - /// - /// Windows message for window close. - /// - private const uint WM_CLOSE = 0x0010; - - /// - /// Windows message for window destruction. - /// - private const uint WM_DESTROY = 0x0002; - - /// - /// Power broadcast event for system suspend. - /// + // Power broadcast event for system suspend. private const uint PBT_APMSUSPEND = 0x0004; - - /// - /// Power broadcast event for automatic resume from suspend. - /// + // Power broadcast event for automatic resume from suspend. private const uint PBT_APMRESUMEAUTOMATIC = 0x0012; - - /// - /// Power broadcast event for resume from suspend. - /// + // Power broadcast event for resume from suspend. private const uint PBT_APMRESUMESUSPEND = 0x0007; - /// - /// Power broadcast event for power setting change. - /// - private const uint PBT_POWERSETTINGCHANGE = 0x8013; + // Flag indicating that the recipient is a callback routine. + private const uint DEVICE_NOTIFY_CALLBACK = 2; /// - /// Flag for registering power setting notification with a window handle. - /// - private const uint DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000; - - /// - /// Handle to the message-only window. - /// - private static readonly IntPtr HWND_MESSAGE = new IntPtr(-3); - - /// - /// Delegate for the window procedure function that processes window messages. - /// - /// Handle to the window. - /// Message identifier. - /// Additional message information. - /// Additional message information. - /// Result of message processing. - private delegate IntPtr WndProc(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam); - - /// - /// Represents the window class structure used for registering a window class. - /// - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct WNDCLASSEX - { - /// - /// The size, in bytes, of this structure. - /// - public uint cbSize; - - /// - /// The class style(s). - /// - public uint style; - - /// - /// A pointer to the window procedure. - /// - public WndProc lpfnWndProc; - - /// - /// The number of extra bytes to allocate following the window-class structure. - /// - public int cbClsExtra; - - /// - /// The number of extra bytes to allocate following the window instance. - /// - public int cbWndExtra; - - /// - /// A handle to the instance that contains the window procedure for the class. - /// - public IntPtr hInstance; - - /// - /// A handle to the class icon. - /// - public IntPtr hIcon; - - /// - /// A handle to the class cursor. - /// - public IntPtr hCursor; - - /// - /// A handle to the class background brush. - /// - public IntPtr hbrBackground; - - /// - /// Pointer to a null-terminated character string that specifies the resource name of the class menu. - /// - [MarshalAs(UnmanagedType.LPWStr)] public string? lpszMenuName; - - /// - /// A pointer to a null-terminated string or is an atom. - /// - [MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName; - - /// - /// A handle to a small icon that is associated with the window class. - /// - public IntPtr hIconSm; - } - - /// - /// Represents a point with x and y coordinates. + /// Structure used to subscribe to suspend/resume notifications via callback. /// [StructLayout(LayoutKind.Sequential)] - private struct POINT + private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { - /// - /// The x-coordinate of the point. - /// - public int x; - - /// - /// The y-coordinate of the point. - /// - public int y; + public DEVICE_NOTIFY_CALLBACK_ROUTINE Callback; + public IntPtr Context; } /// - /// Represents a Windows message structure. + /// Callback routine signature for device/power notifications. + /// Return STATUS_SUCCESS (0) on success. /// - [StructLayout(LayoutKind.Sequential)] - private struct MSG - { - /// - /// A handle to the window whose window procedure receives the message. - /// - public IntPtr hwnd; - - /// - /// The message identifier. - /// - public uint message; - - /// - /// Additional message information. - /// - public UIntPtr wParam; - - /// - /// Additional message information. - /// - public IntPtr lParam; - - /// - /// The time at which the message was posted. - /// - public uint time; - - /// - /// The cursor position, in screen coordinates, when the message was posted. - /// - public POINT pt; - } + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate uint DEVICE_NOTIFY_CALLBACK_ROUTINE(IntPtr Context, uint Type, IntPtr Setting); /// - /// Registers a window class for subsequent use in calls to the CreateWindowEx function. + /// Registers to receive power suspend/resume notifications via a callback. /// - /// Pointer to a WNDCLASSEX structure containing the class information. - /// If the function succeeds, the return value is a class atom that uniquely identifies the class being registered. - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); + /// Must be DEVICE_NOTIFY_CALLBACK for callback delivery. + /// Callback and context parameters. + /// Out registration handle. + /// STATUS_SUCCESS (0) on success. + [DllImport("powrprof.dll", SetLastError = true)] + private static extern uint PowerRegisterSuspendResumeNotification( + uint Flags, + ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS Parameters, + out IntPtr Handle); /// - /// Creates an overlapped, pop-up, or child window with an extended window style. + /// Unregisters a previous suspend/resume notification registration. /// - /// The extended window style of the window being created. - /// A null-terminated string or a class atom created by a previous call to RegisterClassEx. - /// The window name. - /// The style of the window being created. - /// The initial horizontal position of the window. - /// The initial vertical position of the window. - /// The width, in device units, of the window. - /// The height, in device units, of the window. - /// A handle to the parent or owner window of the window being created. - /// A handle to a menu, or specifies a child-window identifier. - /// A handle to the instance of the module to be associated with the window. - /// Pointer to a value to be passed to the window through the CREATESTRUCT structure. - /// If the function succeeds, the return value is a handle to the new window. - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern IntPtr CreateWindowEx( - int dwExStyle, - string lpClassName, - string lpWindowName, - int dwStyle, - int X, - int Y, - int nWidth, - int nHeight, - IntPtr hWndParent, - IntPtr hMenu, - IntPtr hInstance, - IntPtr lpParam); - - /// - /// Retrieves a module handle for the specified module. - /// - /// The name of the loaded module (either a .dll or .exe file). - /// If the function succeeds, the return value is a handle to the specified module. - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr GetModuleHandle(string? lpModuleName); - - /// - /// Retrieves a message from the calling thread's message queue. - /// - /// Pointer to an MSG structure that receives message information. - /// Handle to the window whose messages are to be retrieved. - /// The integer value of the lowest message value to be retrieved. - /// The integer value of the highest message value to be retrieved. - /// If the function retrieves a message other than WM_QUIT, the return value is nonzero. - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); - - /// - /// Translates virtual-key messages into character messages. - /// - /// Pointer to an MSG structure that contains message information retrieved from GetMessage. - /// If the message is translated, the return value is nonzero. - [DllImport("user32.dll")] - private static extern bool TranslateMessage([In] ref MSG lpMsg); - - /// - /// Dispatches a message to a window procedure. - /// - /// Pointer to an MSG structure that contains the message. - /// The return value specifies the value returned by the window procedure. - [DllImport("user32.dll")] - private static extern IntPtr DispatchMessage([In] ref MSG lpMsg); - - /// - /// Calls the default window procedure to provide default processing for any window messages that an application does not process. - /// - /// Handle to the window procedure that received the message. - /// The message. - /// Additional message information. - /// Additional message information. - /// The return value is the result of the message processing and depends on the message. - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, UIntPtr wParam, IntPtr lParam); - - /// - /// Places a message in the message queue associated with the thread that created the specified window. - /// - /// Handle to the window whose window procedure is to receive the message. - /// The message to be posted. - /// Additional message-specific information. - /// Additional message-specific information. - /// If the function succeeds, the return value is nonzero. - [DllImport("user32.dll")] - private static extern bool PostMessage(IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam); - - /// - /// Indicates to the system that a thread has made a request to terminate. - /// - /// The application exit code. - [DllImport("user32.dll")] - private static extern void PostQuitMessage(int nExitCode); - - /// - /// Registers the application to receive power setting notifications for the specified power setting event. - /// - /// Handle to the window or service that will receive the notifications. - /// The GUID of the power setting for which notifications are to be sent. - /// Flags that specify the recipient and the type of notifications to send. - /// If the function succeeds, the return value is a handle to the registration. - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr RegisterPowerSettingNotification(IntPtr hRecipient, ref Guid PowerSettingGuid, uint Flags); - - /// - /// Unregisters the power setting notification. - /// - /// Handle to the registration returned by RegisterPowerSettingNotification. - /// If the function succeeds, the return value is nonzero. - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool UnregisterPowerSettingNotification(IntPtr Handle); + /// The registration handle. + /// STATUS_SUCCESS (0) on success. + [DllImport("powrprof.dll", SetLastError = true)] + private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr Handle); } \ No newline at end of file From cbc327f09df2765ed23f1d44d1e7f9ea02472513 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 18 Oct 2025 13:59:48 +0200 Subject: [PATCH 4/5] Fixed not activating new powermode provider if using Windows older than 8.0 --- Duplicati/Library/Snapshots/PowerModeUtility.cs | 4 +++- Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Duplicati/Library/Snapshots/PowerModeUtility.cs b/Duplicati/Library/Snapshots/PowerModeUtility.cs index bb7404e74..197f8f1a2 100644 --- a/Duplicati/Library/Snapshots/PowerModeUtility.cs +++ b/Duplicati/Library/Snapshots/PowerModeUtility.cs @@ -44,7 +44,9 @@ public static class PowerModeUtility PowerModeProvider.Net => new Windows.WindowsPowerModeProvider(), PowerModeProvider.Native or PowerModeProvider.Default => - Windows.WindowsShimLoader.NewPowerModeProvider(), + OperatingSystem.IsWindowsVersionAtLeast(8, 0) ? + Windows.WindowsShimLoader.NewPowerModeProvider() + : new Windows.WindowsPowerModeProvider(), _ => null }; diff --git a/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs b/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs index 64e52c4ba..56ab16b31 100644 --- a/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs +++ b/Duplicati/Library/Snapshots/Windows/WindowsShimLoader.cs @@ -27,7 +27,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; -using System.Text; using Duplicati.Library.Interface; using Duplicati.Library.Utility; From 8534c56581f737a8400c846fbefdc2a753199db5 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 18 Oct 2025 14:04:35 +0200 Subject: [PATCH 5/5] Cleaned up comments --- .../WindowsModules/PowerManagementModule.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Duplicati/Library/WindowsModules/PowerManagementModule.cs b/Duplicati/Library/WindowsModules/PowerManagementModule.cs index f87559225..1173d321f 100644 --- a/Duplicati/Library/WindowsModules/PowerManagementModule.cs +++ b/Duplicati/Library/WindowsModules/PowerManagementModule.cs @@ -29,8 +29,7 @@ using Duplicati.Library.Interface; namespace Duplicati.Library.WindowsModules; /// -/// Provides power management functionality for Windows using the powrprof callback API. -/// Eliminates the hidden window by registering a suspend/resume callback (Windows 8+). +/// Provides power management functionality for Windows using the powrprof callback API (Windows 8+). /// [SupportedOSPlatform("windows")] public sealed class PowerManagementModule : IPowerModeProvider, IDisposable @@ -90,6 +89,11 @@ public sealed class PowerManagementModule : IPowerModeProvider, IDisposable } } + /// + /// Constant indicating successful operation. + /// + private const uint STATUS_SUCCESS = 0; + /// /// Callback invoked by the system for suspend/resume notifications. /// @@ -97,7 +101,6 @@ public sealed class PowerManagementModule : IPowerModeProvider, IDisposable /// Power event type (e.g., PBT_APMSUSPEND, PBT_APMRESUMEAUTOMATIC). /// Additional info (unused). /// STATUS_SUCCESS (0) on success. - private static uint STATUS_SUCCESS => 0; private uint SuspendResumeCallback(IntPtr context, uint type, IntPtr setting) { switch (type) @@ -127,16 +130,22 @@ public sealed class PowerManagementModule : IPowerModeProvider, IDisposable _callbackRef = null; } - // Interop - - // Power broadcast event for system suspend. + /// + /// Power broadcast event for system suspend. + /// private const uint PBT_APMSUSPEND = 0x0004; - // Power broadcast event for automatic resume from suspend. + /// + /// Power broadcast event for automatic resume from suspend. + /// private const uint PBT_APMRESUMEAUTOMATIC = 0x0012; - // Power broadcast event for resume from suspend. + /// + /// Power broadcast event for resume from suspend. + /// private const uint PBT_APMRESUMESUSPEND = 0x0007; - // Flag indicating that the recipient is a callback routine. + /// + /// Flag indicating that the recipient is a callback routine. + /// private const uint DEVICE_NOTIFY_CALLBACK = 2; /// @@ -145,7 +154,13 @@ public sealed class PowerManagementModule : IPowerModeProvider, IDisposable [StructLayout(LayoutKind.Sequential)] private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { + /// + /// The callback routine to receive notifications. + /// public DEVICE_NOTIFY_CALLBACK_ROUTINE Callback; + /// + /// User-defined context passed to the callback. + /// public IntPtr Context; }