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..197f8f1a2 --- /dev/null +++ b/Duplicati/Library/Snapshots/PowerModeUtility.cs @@ -0,0 +1,56 @@ +// 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 => + OperatingSystem.IsWindowsVersionAtLeast(8, 0) ? + Windows.WindowsShimLoader.NewPowerModeProvider() + : new Windows.WindowsPowerModeProvider(), + _ => 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..603a5c9fe --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/WindowsPowerModeProvider.cs @@ -0,0 +1,73 @@ +// 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.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..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; @@ -47,7 +46,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 +99,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 +153,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..1173d321f --- /dev/null +++ b/Duplicati/Library/WindowsModules/PowerManagementModule.cs @@ -0,0 +1,194 @@ +// 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.Runtime.InteropServices; +using System.Runtime.Versioning; +using Duplicati.Library.Interface; + +namespace Duplicati.Library.WindowsModules; + +/// +/// Provides power management functionality for Windows using the powrprof callback API (Windows 8+). +/// +[SupportedOSPlatform("windows")] +public sealed class PowerManagementModule : IPowerModeProvider, IDisposable +{ + /// + /// Registration handle returned from PowerRegisterSuspendResumeNotification. + /// + private IntPtr _registrationHandle = IntPtr.Zero; + + /// + /// Keep a reference to the delegate to prevent it from being garbage collected. + /// + private DEVICE_NOTIFY_CALLBACK_ROUTINE? _callbackRef; + + /// + public Action? OnResume { get; set; } + + /// + public Action? OnSuspend { get; set; } + + /// + /// Initializes a new instance. Required for reflection-based loading. + /// + public PowerManagementModule() : this(null) + { + } + + /// + /// Initializes a new instance. The parameter is ignored in this implementation. + /// + /// Unused. Present for compatibility with previous constructor. + public PowerManagementModule(Guid? _) + { + RegisterSuspendResumeCallback(); + } + + /// + /// Registers the suspend/resume callback using powrprof (Windows 8+). + /// + private void RegisterSuspendResumeCallback() + { + _callbackRef = new DEVICE_NOTIFY_CALLBACK_ROUTINE(SuspendResumeCallback); + var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS + { + Callback = _callbackRef, + Context = IntPtr.Zero + }; + + // 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) + { + _registrationHandle = IntPtr.Zero; + } + } + + /// + /// Constant indicating successful operation. + /// + private const uint STATUS_SUCCESS = 0; + + /// + /// Callback invoked by the system for suspend/resume notifications. + /// + /// User-provided context (unused). + /// Power event type (e.g., PBT_APMSUSPEND, PBT_APMRESUMEAUTOMATIC). + /// Additional info (unused). + /// STATUS_SUCCESS (0) on success. + private uint SuspendResumeCallback(IntPtr context, uint type, IntPtr setting) + { + switch (type) + { + case PBT_APMSUSPEND: + OnSuspend?.Invoke(); + break; + + case PBT_APMRESUMEAUTOMATIC: + case PBT_APMRESUMESUSPEND: + OnResume?.Invoke(); + break; + } + + return STATUS_SUCCESS; + } + + /// + public void Dispose() + { + if (_registrationHandle != IntPtr.Zero) + { + PowerUnregisterSuspendResumeNotification(_registrationHandle); + _registrationHandle = IntPtr.Zero; + } + + _callbackRef = null; + } + + /// + /// 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; + + /// + /// Flag indicating that the recipient is a callback routine. + /// + private const uint DEVICE_NOTIFY_CALLBACK = 2; + + /// + /// Structure used to subscribe to suspend/resume notifications via callback. + /// + [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; + } + + /// + /// Callback routine signature for device/power notifications. + /// Return STATUS_SUCCESS (0) on success. + /// + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate uint DEVICE_NOTIFY_CALLBACK_ROUTINE(IntPtr Context, uint Type, IntPtr Setting); + + /// + /// Registers to receive power suspend/resume notifications via a callback. + /// + /// 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); + + /// + /// Unregisters a previous suspend/resume notification registration. + /// + /// 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 diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 8cdddbca2..cc7971660 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -236,7 +236,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)) { @@ -857,7 +857,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) @@ -873,6 +873,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, }; } }