// 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 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.
///
[SupportedOSPlatform("windows")]
public sealed class PowerManagementModule : IPowerModeProvider, IDisposable
{
///
/// The background thread that runs the message loop for handling Windows messages.
///
private readonly Thread _thread;
///
/// 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; }
///
/// 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.
///
public PowerManagementModule() : this(null)
{
}
///
/// 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();
}
///
/// Runs the message loop for handling Windows messages, including power broadcast events.
/// Creates a hidden window and optionally subscribes to power setting notifications.
///
/// 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,
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;
}
}
///
/// 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 != IntPtr.Zero)
{
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);
}