// 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); }