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,
};
}
}