Refactor logging, state management, and IPC

Improved logging consistency by replacing verbose debug logs with concise warnings and errors where appropriate. Introduced a debounced-save strategy in `MonitorStateManager` to optimize disk I/O during rapid updates. Removed the `PowerDisplayProcessManager` class and named pipe-based IPC, indicating a significant architectural shift.

Translated all comments from Chinese to English for better readability. Simplified and refactored initialization logic in `MainWindow.xaml.cs` for better maintainability. Removed unused code, including system tray-related structures and imports, and improved overall code clarity and consistency.
This commit is contained in:
Yu Leng
2025-11-13 14:14:49 +08:00
parent d822745c98
commit 33d5ff26c6
13 changed files with 183 additions and 750 deletions

View File

@@ -63,7 +63,7 @@ namespace PowerDisplay.Core
} }
else else
{ {
Logger.LogInfo("WMI brightness control not available on this system"); Logger.LogWarning("WMI brightness control not available on this system");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -199,31 +199,23 @@ namespace PowerDisplay.Core
/// </summary> /// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default) public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
{ {
Logger.LogDebug($"[MonitorManager] SetBrightnessAsync called for {monitorId}, brightness={brightness}");
var monitor = GetMonitor(monitorId); var monitor = GetMonitor(monitorId);
if (monitor == null) if (monitor == null)
{ {
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}"); Logger.LogError($"Monitor not found: {monitorId}");
return MonitorOperationResult.Failure("Monitor not found"); return MonitorOperationResult.Failure("Monitor not found");
} }
Logger.LogDebug($"[MonitorManager] Monitor found: {monitor.Id}, Type={monitor.Type}, Handle=0x{monitor.Handle:X}, DeviceKey={monitor.DeviceKey}");
var controller = GetControllerForMonitor(monitor); var controller = GetControllerForMonitor(monitor);
if (controller == null) if (controller == null)
{ {
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}"); Logger.LogError($"No controller available for monitor {monitorId}, Type={monitor.Type}");
return MonitorOperationResult.Failure("No controller available for this monitor"); return MonitorOperationResult.Failure("No controller available for this monitor");
} }
Logger.LogDebug($"[MonitorManager] Controller found: {controller.GetType().Name}");
try try
{ {
Logger.LogDebug($"[MonitorManager] Calling controller.SetBrightnessAsync for {monitor.Id}");
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken); var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
Logger.LogDebug($"[MonitorManager] controller.SetBrightnessAsync returned: IsSuccess={result.IsSuccess}, ErrorMessage={result.ErrorMessage}");
if (result.IsSuccess) if (result.IsSuccess)
{ {
@@ -344,8 +336,6 @@ namespace PowerDisplay.Core
// This is a rough mapping - actual values depend on monitor implementation // This is a rough mapping - actual values depend on monitor implementation
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum); var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
monitor.CurrentColorTemperature = kelvin; monitor.CurrentColorTemperature = kelvin;
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
} }
} }
} }

View File

@@ -25,8 +25,11 @@ namespace PowerDisplay.Helpers
private readonly string _stateFilePath; private readonly string _stateFilePath;
private readonly Dictionary<string, MonitorState> _states = new(); private readonly Dictionary<string, MonitorState> _states = new();
private readonly object _lock = new object(); private readonly object _lock = new object();
private readonly Timer _saveTimer;
private bool _disposed; private bool _disposed;
private bool _isDirty;
private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
/// <summary> /// <summary>
/// Monitor state data (internal tracking, not serialized) /// Monitor state data (internal tracking, not serialized)
@@ -55,16 +58,40 @@ namespace PowerDisplay.Helpers
_stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName); _stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName);
// Initialize debounce timer (disabled initially)
_saveTimer = new Timer(OnSaveTimerElapsed, null, Timeout.Infinite, Timeout.Infinite);
// Load existing state if available // Load existing state if available
LoadStateFromDisk(); LoadStateFromDisk();
Logger.LogInfo($"MonitorStateManager initialized with direct-save strategy, state file: {_stateFilePath}"); Logger.LogInfo($"MonitorStateManager initialized with debounced-save strategy (debounce: {SaveDebounceMs}ms), state file: {_stateFilePath}");
} }
/// <summary> /// <summary>
/// Update monitor parameter and save immediately to disk. /// Timer callback to save state when dirty
/// </summary>
private void OnSaveTimerElapsed(object? state)
{
bool shouldSave = false;
lock (_lock)
{
if (_isDirty && !_disposed)
{
shouldSave = true;
_isDirty = false;
}
}
if (shouldSave)
{
SaveStateToDisk();
}
}
/// <summary>
/// Update monitor parameter and schedule debounced save to disk.
/// Uses HardwareId as the stable key. /// Uses HardwareId as the stable key.
/// Direct-save strategy ensures no data loss and simplifies code (KISS principle). /// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
/// </summary> /// </summary>
public void UpdateMonitorParameter(string hardwareId, string property, int value) public void UpdateMonitorParameter(string hardwareId, string property, int value)
{ {
@@ -104,12 +131,15 @@ namespace PowerDisplay.Helpers
Logger.LogWarning($"Unknown property: {property}"); Logger.LogWarning($"Unknown property: {property}");
return; return;
} }
// Mark dirty and schedule debounced save
_isDirty = true;
} }
// Save immediately after update - simple and reliable! // Reset timer to debounce rapid updates (e.g., during slider drag)
SaveStateToDisk(); _saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
Logger.LogTrace($"[State] Updated and saved {property}={value} for monitor HardwareId='{hardwareId}'"); Logger.LogTrace($"[State] Updated {property}={value} for monitor HardwareId='{hardwareId}', save scheduled");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -201,7 +231,7 @@ namespace PowerDisplay.Helpers
/// <summary> /// <summary>
/// Save current state to disk immediately. /// Save current state to disk immediately.
/// Simplified direct-save approach - no timer, no dirty flags, just save! /// Called by timer after debounce period or on dispose to flush pending changes.
/// </summary> /// </summary>
private void SaveStateToDisk() private void SaveStateToDisk()
{ {
@@ -257,12 +287,26 @@ namespace PowerDisplay.Helpers
return; return;
} }
// Stop the timer first
_saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
bool wasDirty = false;
lock (_lock) lock (_lock)
{ {
wasDirty = _isDirty;
_disposed = true; _disposed = true;
_isDirty = false;
} }
// State is already saved with each update, no need for final flush! // Flush any pending changes before disposing
if (wasDirty)
{
Logger.LogInfo("Flushing pending state changes before dispose");
SaveStateToDisk();
}
_saveTimer?.Dispose();
Logger.LogInfo("MonitorStateManager disposed"); Logger.LogInfo("MonitorStateManager disposed");
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PowerDisplay.Helpers;
public static class NamedPipeProcessor
{
public static async Task ProcessNamedPipeAsync(string pipeName, TimeSpan connectTimeout, Action<string> messageHandler, CancellationToken cancellationToken)
{
using NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.In);
await pipeClient.ConnectAsync(connectTimeout, cancellationToken);
using StreamReader streamReader = new(pipeClient, Encoding.Unicode);
while (true)
{
var message = await streamReader.ReadLineAsync(cancellationToken);
if (message != null)
{
messageHandler(message);
}
var intraMessageDelay = TimeSpan.FromMilliseconds(10);
await Task.Delay(intraMessageDelay, cancellationToken);
}
}
}

View File

@@ -224,7 +224,6 @@ namespace PowerDisplay.Native.DDC
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle); var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue)) if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue))
{ {
Logger.LogInfo($"Successfully set color temperature to {colorTemperature}K via DDC/CI (VCP 0x{vcpCode.Value:X2})");
return MonitorOperationResult.Success(); return MonitorOperationResult.Success();
} }
@@ -326,25 +325,20 @@ namespace PowerDisplay.Native.DDC
{ {
// Get all display devices with stable device IDs (Twinkle Tray style) // Get all display devices with stable device IDs (Twinkle Tray style)
var displayDevices = DdcCiNative.GetAllDisplayDevices(); var displayDevices = DdcCiNative.GetAllDisplayDevices();
Logger.LogInfo($"DDC: Found {displayDevices.Count} display devices via EnumDisplayDevices");
// Also get hardware info for friendly names // Also get hardware info for friendly names
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo(); var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
Logger.LogDebug($"DDC: GetAllMonitorDisplayInfo returned {monitorDisplayInfo.Count} items");
// Enumerate all monitors // Enumerate all monitors
var monitorHandles = new List<IntPtr>(); var monitorHandles = new List<IntPtr>();
Logger.LogDebug($"DDC: About to call EnumDisplayMonitors...");
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData) bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
{ {
Logger.LogDebug($"DDC: EnumProc callback - hMonitor=0x{hMonitor:X}");
monitorHandles.Add(hMonitor); monitorHandles.Add(hMonitor);
return true; return true;
} }
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero); bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
Logger.LogDebug($"DDC: EnumDisplayMonitors returned {enumResult}, found {monitorHandles.Count} monitor handles");
if (!enumResult) if (!enumResult)
{ {
@@ -371,7 +365,6 @@ namespace PowerDisplay.Native.DDC
{ {
if (attempt > 0) if (attempt > 0)
{ {
Logger.LogInfo($"DDC: Retry attempt {attempt}/{maxRetries - 1} for hMonitor 0x{hMonitor:X} after {retryDelayMs}ms delay");
await Task.Delay(retryDelayMs, cancellationToken); await Task.Delay(retryDelayMs, cancellationToken);
} }
@@ -402,11 +395,6 @@ namespace PowerDisplay.Native.DDC
if (!hasNullHandle) if (!hasNullHandle)
{ {
// Success! All handles are valid // Success! All handles are valid
if (attempt > 0)
{
Logger.LogInfo($"DDC: Successfully obtained valid handles on attempt {attempt + 1}");
}
break; break;
} }
else if (attempt < maxRetries - 1) else if (attempt < maxRetries - 1)
@@ -474,7 +462,6 @@ namespace PowerDisplay.Native.DDC
var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice); var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice);
if (monitor != null) if (monitor != null)
{ {
Logger.LogInfo($"DDC: Created monitor {monitor.Id} with handle 0x{monitor.Handle:X} (reused: {reusingOldHandle}), deviceKey: {monitor.DeviceKey}");
monitors.Add(monitor); monitors.Add(monitor);
// Store in new map for cleanup // Store in new map for cleanup
@@ -492,7 +479,6 @@ namespace PowerDisplay.Native.DDC
} }
finally finally
{ {
Logger.LogDebug($"DDC: DiscoverMonitorsAsync returning {monitors.Count} monitors");
} }
return monitors; return monitors;

View File

@@ -10,7 +10,7 @@ using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates; using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke; using static PowerDisplay.Native.PInvoke;
// 类型别名,兼容 Windows API 命名约定 // Type aliases for Windows API naming conventions compatibility
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice; using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER; using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO; using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
@@ -27,7 +27,7 @@ using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native.DDC namespace PowerDisplay.Native.DDC
{ {
/// <summary> /// <summary>
/// 显示设备信息类 /// Display device information class
/// </summary> /// </summary>
public class DisplayDeviceInfo public class DisplayDeviceInfo
{ {
@@ -41,11 +41,11 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// DDC/CI 原生 API 封装 /// DDC/CI native API wrapper
/// </summary> /// </summary>
public static class DdcCiNative public static class DdcCiNative
{ {
// Display Configuration 常量 // Display Configuration constants
public const uint QdcAllPaths = 0x00000001; public const uint QdcAllPaths = 0x00000001;
public const uint QdcOnlyActivePaths = 0x00000002; public const uint QdcOnlyActivePaths = 0x00000002;
@@ -55,13 +55,13 @@ namespace PowerDisplay.Native.DDC
// Helper Methods // Helper Methods
/// <summary> /// <summary>
/// 获取 VCP 功能值的安全包装 /// Safe wrapper for getting VCP feature value
/// </summary> /// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param> /// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="vcpCode">VCP 代码</param> /// <param name="vcpCode">VCP code</param>
/// <param name="currentValue">当前值</param> /// <param name="currentValue">Current value</param>
/// <param name="maxValue">最大值</param> /// <param name="maxValue">Maximum value</param>
/// <returns>是否成功</returns> /// <returns>True if successful</returns>
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue) public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
{ {
currentValue = 0; currentValue = 0;
@@ -83,12 +83,12 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// 设置 VCP 功能值的安全包装 /// Safe wrapper for setting VCP feature value
/// </summary> /// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param> /// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="vcpCode">VCP 代码</param> /// <param name="vcpCode">VCP code</param>
/// <param name="value">新值</param> /// <param name="value">New value</param>
/// <returns>是否成功</returns> /// <returns>True if successful</returns>
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value) public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
{ {
if (hPhysicalMonitor == IntPtr.Zero) if (hPhysicalMonitor == IntPtr.Zero)
@@ -107,13 +107,13 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// 获取高级亮度信息的安全包装 /// Safe wrapper for getting advanced brightness information
/// </summary> /// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param> /// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="minBrightness">最小亮度</param> /// <param name="minBrightness">Minimum brightness</param>
/// <param name="currentBrightness">当前亮度</param> /// <param name="currentBrightness">Current brightness</param>
/// <param name="maxBrightness">最大亮度</param> /// <param name="maxBrightness">Maximum brightness</param>
/// <returns>是否成功</returns> /// <returns>True if successful</returns>
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness) public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
{ {
minBrightness = 0; minBrightness = 0;
@@ -394,10 +394,10 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// 获取所有显示设备信息(使用 EnumDisplayDevices API /// Get all display device information (using EnumDisplayDevices API)
/// Twinkle Tray 实现保持一致 /// Implementation consistent with Twinkle Tray
/// </summary> /// </summary>
/// <returns>显示设备信息列表</returns> /// <returns>List of display device information</returns>
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices() public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
{ {
var devices = new List<DisplayDeviceInfo>(); var devices = new List<DisplayDeviceInfo>();

View File

@@ -42,22 +42,22 @@ namespace PowerDisplay.Native
public const byte VcpCodeMute = 0x8D; public const byte VcpCodeMute = 0x8D;
/// <summary> /// <summary>
/// VCP code: Color temperature request (主要色温控制) /// VCP code: Color temperature request (primary color temperature control)
/// </summary> /// </summary>
public const byte VcpCodeColorTemperature = 0x0C; public const byte VcpCodeColorTemperature = 0x0C;
/// <summary> /// <summary>
/// VCP code: Color temperature increment (色温增量调节) /// VCP code: Color temperature increment (incremental color temperature adjustment)
/// </summary> /// </summary>
public const byte VcpCodeColorTemperatureIncrement = 0x0B; public const byte VcpCodeColorTemperatureIncrement = 0x0B;
/// <summary> /// <summary>
/// VCP code: Gamma correction (Gamma调节) /// VCP code: Gamma correction (gamma adjustment)
/// </summary> /// </summary>
public const byte VcpCodeGamma = 0x72; public const byte VcpCodeGamma = 0x72;
/// <summary> /// <summary>
/// VCP code: Select color preset (颜色预设选择) /// VCP code: Select color preset (color preset selection)
/// </summary> /// </summary>
public const byte VcpCodeSelectColorPreset = 0x14; public const byte VcpCodeSelectColorPreset = 0x14;

View File

@@ -5,31 +5,31 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// 类型别名,兼容 Windows API 命名约定 // Type aliases for Windows API naming conventions compatibility
using RECT = PowerDisplay.Native.Rect; using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native; namespace PowerDisplay.Native;
/// <summary> /// <summary>
/// 委托类型定义 /// Native delegate type definitions
/// </summary> /// </summary>
public static class NativeDelegates public static class NativeDelegates
{ {
/// <summary> /// <summary>
/// 显示器枚举过程委托 /// Monitor enumeration procedure delegate
/// </summary> /// </summary>
/// <param name="hMonitor">显示器句柄</param> /// <param name="hMonitor">Monitor handle</param>
/// <param name="hdcMonitor">显示器 DC</param> /// <param name="hdcMonitor">Monitor device context</param>
/// <param name="lprcMonitor">显示器矩形指针</param> /// <param name="lprcMonitor">Pointer to monitor rectangle</param>
/// <param name="dwData">用户数据</param> /// <param name="dwData">User data</param>
/// <returns>继续枚举返回 true</returns> /// <returns>True to continue enumeration</returns>
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData); public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
/// <summary> /// <summary>
/// 线程启动例程委托 /// Thread start routine delegate
/// </summary> /// </summary>
/// <param name="lpParameter">线程参数</param> /// <param name="lpParameter">Thread parameter</param>
/// <returns>线程退出代码</returns> /// <returns>Thread exit code</returns>
public delegate uint ThreadStartRoutine(IntPtr lpParameter); public delegate uint ThreadStartRoutine(IntPtr lpParameter);
} }

View File

@@ -442,91 +442,4 @@ namespace PowerDisplay.Native
this.Y = y; this.Y = y;
} }
} }
/// <summary>
/// Notify icon data structure (for system tray)
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct NOTIFYICONDATA
{
public uint CbSize;
public IntPtr HWnd;
public uint UID;
public uint UFlags;
public uint UCallbackMessage;
public IntPtr HIcon;
/// <summary>
/// Tooltip text - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort SzTip[128];
public uint DwState;
public uint DwStateMask;
/// <summary>
/// Info balloon text - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort SzInfo[256];
public uint UTimeout;
/// <summary>
/// Info balloon title - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort SzInfoTitle[64];
public uint DwInfoFlags;
/// <summary>
/// Helper method to set tooltip text
/// </summary>
public void SetTip(string tip)
{
fixed (ushort* ptr = SzTip)
{
int length = Math.Min(tip.Length, 127);
for (int i = 0; i < length; i++)
{
ptr[i] = tip[i];
}
ptr[length] = 0; // Null terminator
}
}
/// <summary>
/// Helper method to set info balloon text
/// </summary>
public void SetInfo(string info)
{
fixed (ushort* ptr = SzInfo)
{
int length = Math.Min(info.Length, 255);
for (int i = 0; i < length; i++)
{
ptr[i] = info[i];
}
ptr[length] = 0; // Null terminator
}
}
/// <summary>
/// Helper method to set info balloon title
/// </summary>
public void SetInfoTitle(string title)
{
fixed (ushort* ptr = SzInfoTitle)
{
int length = Math.Min(title.Length, 63);
for (int i = 0; i < length; i++)
{
ptr[i] = title[i];
}
ptr[length] = 0; // Null terminator
}
}
}
} }

View File

@@ -171,13 +171,6 @@ namespace PowerDisplay.Native
POINT pt, POINT pt,
uint dwFlags); uint dwFlags);
// ==================== Shell32.dll - Tray Icon ====================
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool Shell_NotifyIcon(
uint dwMessage,
ref NOTIFYICONDATA lpData);
// ==================== Dxva2.dll - DDC/CI Monitor Control ==================== // ==================== Dxva2.dll - DDC/CI Monitor Control ====================
[LibraryImport("Dxva2.dll")] [LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]

View File

@@ -43,32 +43,32 @@ namespace PowerDisplay.Native
private const int SwRestore = 9; private const int SwRestore = 9;
/// <summary> /// <summary>
/// 禁用窗口的拖动和缩放功能 /// Disable window moving and resizing functionality
/// </summary> /// </summary>
public static void DisableWindowMovingAndResizing(IntPtr hWnd) public static void DisableWindowMovingAndResizing(IntPtr hWnd)
{ {
// 获取当前窗口样式 // Get current window style
#if WIN64 #if WIN64
int style = (int)GetWindowLong(hWnd, GwlStyle); int style = (int)GetWindowLong(hWnd, GwlStyle);
#else #else
int style = GetWindowLong(hWnd, GwlStyle); int style = GetWindowLong(hWnd, GwlStyle);
#endif #endif
// 移除可调整大小的边框、标题栏和系统菜单 // Remove resizable borders, title bar, and system menu
style &= ~WsThickframe; style &= ~WsThickframe;
style &= ~WsMaximizebox; style &= ~WsMaximizebox;
style &= ~WsMinimizebox; style &= ~WsMinimizebox;
style &= ~WsCaption; // 移除整个标题栏 style &= ~WsCaption; // Remove entire title bar
style &= ~WsSysmenu; // 移除系统菜单 style &= ~WsSysmenu; // Remove system menu
// 设置新的窗口样式 // Set new window style
#if WIN64 #if WIN64
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style)); _ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
#else #else
_ = SetWindowLong(hWnd, GwlStyle, style); _ = SetWindowLong(hWnd, GwlStyle, style);
#endif #endif
// 获取扩展样式并移除相关边框 // Get extended style and remove related borders
#if WIN64 #if WIN64
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle); int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
#else #else
@@ -84,7 +84,7 @@ namespace PowerDisplay.Native
_ = SetWindowLong(hWnd, GwlExstyle, exStyle); _ = SetWindowLong(hWnd, GwlExstyle, exStyle);
#endif #endif
// 刷新窗口框架 // Refresh window frame
SetWindowPos( SetWindowPos(
hWnd, hWnd,
IntPtr.Zero, IntPtr.Zero,
@@ -96,7 +96,7 @@ namespace PowerDisplay.Native
} }
/// <summary> /// <summary>
/// 设置窗口是否置顶 /// Set whether window is topmost
/// </summary> /// </summary>
public static void SetWindowTopmost(IntPtr hWnd, bool topmost) public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
{ {
@@ -111,7 +111,7 @@ namespace PowerDisplay.Native
} }
/// <summary> /// <summary>
/// 显示或隐藏窗口 /// Show or hide window
/// </summary> /// </summary>
public static void ShowWindow(IntPtr hWnd, bool show) public static void ShowWindow(IntPtr hWnd, bool show)
{ {
@@ -119,7 +119,7 @@ namespace PowerDisplay.Native
} }
/// <summary> /// <summary>
/// 最小化窗口 /// Minimize window
/// </summary> /// </summary>
public static void MinimizeWindow(IntPtr hWnd) public static void MinimizeWindow(IntPtr hWnd)
{ {
@@ -127,7 +127,7 @@ namespace PowerDisplay.Native
} }
/// <summary> /// <summary>
/// 恢复窗口 /// Restore window
/// </summary> /// </summary>
public static void RestoreWindow(IntPtr hWnd) public static void RestoreWindow(IntPtr hWnd)
{ {
@@ -135,28 +135,28 @@ namespace PowerDisplay.Native
} }
/// <summary> /// <summary>
/// 设置窗口不在任务栏显示 /// Hide window from taskbar
/// </summary> /// </summary>
public static void HideFromTaskbar(IntPtr hWnd) public static void HideFromTaskbar(IntPtr hWnd)
{ {
// 获取当前扩展样式 // Get current extended style
#if WIN64 #if WIN64
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle); int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
#else #else
int exStyle = GetWindowLong(hWnd, GwlExstyle); int exStyle = GetWindowLong(hWnd, GwlExstyle);
#endif #endif
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示 // Add WS_EX_TOOLWINDOW style to hide window from taskbar
exStyle |= WsExToolwindow; exStyle |= WsExToolwindow;
// 设置新的扩展样式 // Set new extended style
#if WIN64 #if WIN64
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle)); _ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
#else #else
_ = SetWindowLong(hWnd, GwlExstyle, exStyle); _ = SetWindowLong(hWnd, GwlExstyle, exStyle);
#endif #endif
// 刷新窗口框架 // Refresh window frame
SetWindowPos( SetWindowPos(
hWnd, hWnd,
IntPtr.Zero, IntPtr.Zero,

View File

@@ -51,21 +51,34 @@ namespace PowerDisplay
{ {
this.InitializeComponent(); this.InitializeComponent();
// Lightweight initialization - no heavy operations in constructor // 1. Configure window immediately (synchronous, no data dependency)
// Setup window properties ConfigureWindow();
SetupWindow();
// Initialize UI text // 2. Initialize UI text (synchronous, lightweight)
InitializeUIText(); InitializeUIText();
// Clean up resources on window close // 3. Create ViewModel immediately (lightweight object, no scanning yet)
this.Closed += OnWindowClosed; _viewModel = new MainViewModel();
RootGrid.DataContext = _viewModel;
Bindings.Update();
// Auto-hide window when it loses focus (click outside) // 4. Register event handlers
this.Activated += OnWindowActivated; RegisterEventHandlers();
// Delay ViewModel creation until first activation (async) // 5. Start background initialization (don't wait)
this.Activated += OnFirstActivated; _ = Task.Run(async () =>
{
try
{
await InitializeAsync();
_hasInitialized = true;
}
catch (Exception ex)
{
Logger.LogError($"Background initialization failed: {ex.Message}");
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
}
});
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -74,22 +87,31 @@ namespace PowerDisplay
} }
} }
private bool _hasInitialized; /// <summary>
/// Register all event handlers for window and ViewModel
private async void OnFirstActivated(object sender, WindowActivatedEventArgs args) /// </summary>
private void RegisterEventHandlers()
{ {
// Only initialize once on first activation // Window events
if (_hasInitialized) this.Closed += OnWindowClosed;
{ this.Activated += OnWindowActivated;
return;
}
await EnsureInitializedAsync(); // ViewModel events
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
// Button events
LinkButton.Click += OnLinkClick;
DisableButton.Click += OnDisableClick;
RefreshButton.Click += OnRefreshClick;
} }
private bool _hasInitialized;
/// <summary> /// <summary>
/// Ensures the window is properly initialized with ViewModel and data /// Ensures the window is properly initialized with ViewModel and data
/// Can be called from external code (e.g., App startup) to initialize in background /// Can be called from external code (e.g., App startup) to pre-initialize in background
/// </summary> /// </summary>
public async Task EnsureInitializedAsync() public async Task EnsureInitializedAsync()
{ {
@@ -98,57 +120,32 @@ namespace PowerDisplay
return; return;
} }
_hasInitialized = true; // Wait for background initialization to complete
this.Activated -= OnFirstActivated; // Unsubscribe after first run // This is a no-op if initialization already completed
// Create and initialize ViewModel asynchronously
// This will trigger Loading UI (IsScanning) during monitor discovery
_viewModel = new MainViewModel();
RootGrid.DataContext = _viewModel;
// Notify bindings that ViewModel is now available (for x:Bind)
Bindings.Update();
// Initialize ViewModel event handlers
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
// Bind button events
LinkButton.Click += OnLinkClick;
DisableButton.Click += OnDisableClick;
RefreshButton.Click += OnRefreshClick;
// Start async initialization (monitor scanning happens here)
await InitializeAsync(); await InitializeAsync();
_hasInitialized = true;
// FIX BUG #4: Don't auto-hide window after initialization
// Window visibility should be controlled by IPC commands (show_window/toggle_window)
// If launched via PowerToys Runner, window should start hidden and wait for IPC show command
// If launched standalone, window should stay visible
// HideWindow(); // REMOVED - controlled by IPC or standalone mode
} }
private async Task InitializeAsync() private async Task InitializeAsync()
{ {
try try
{ {
// No delays! Direct async operation // Perform monitor scanning and settings reload
await _viewModel.RefreshMonitorsAsync(); await _viewModel.RefreshMonitorsAsync();
await _viewModel.ReloadMonitorSettingsAsync(); await _viewModel.ReloadMonitorSettingsAsync();
// Adjust window size after data is loaded (event-driven) // Adjust window size after data is loaded (must run on UI thread)
AdjustWindowSizeToContent(); DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
} }
catch (WmiLight.WmiException ex) catch (WmiLight.WmiException ex)
{ {
Logger.LogError($"WMI access failed: {ex.Message}"); Logger.LogError($"WMI access failed: {ex.Message}");
ShowError("Unable to access internal display control, administrator privileges may be required."); DispatcherQueue.TryEnqueue(() => ShowError("Unable to access internal display control, administrator privileges may be required."));
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"Initialization failed: {ex.Message}"); Logger.LogError($"Initialization failed: {ex.Message}");
ShowError($"Initialization failed: {ex.Message}"); DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
} }
} }
@@ -199,7 +196,6 @@ namespace PowerDisplay
// Auto-hide window when it loses focus (deactivated) // Auto-hide window when it loses focus (deactivated)
if (args.WindowActivationState == WindowActivationState.Deactivated) if (args.WindowActivationState == WindowActivationState.Deactivated)
{ {
Logger.LogInfo("[DEACTIVATED] Window lost focus, hiding");
HideWindow(); HideWindow();
} }
} }
@@ -228,80 +224,54 @@ namespace PowerDisplay
public void ShowWindow() public void ShowWindow()
{ {
Logger.LogInfo("[SHOWWINDOW] Method entry");
Logger.LogInfo($"[SHOWWINDOW] _hasInitialized: {_hasInitialized}");
Logger.LogInfo($"[SHOWWINDOW] Current thread ID: {Environment.CurrentManagedThreadId}");
try try
{ {
// If not initialized, log warning but continue showing // If not initialized, log warning but continue showing
if (!_hasInitialized) if (!_hasInitialized)
{ {
Logger.LogWarning("[SHOWWINDOW] Window not fully initialized yet, showing anyway"); Logger.LogWarning("Window not fully initialized yet, showing anyway");
} }
Logger.LogInfo("[SHOWWINDOW] Getting window handle");
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
Logger.LogInfo($"[SHOWWINDOW] Window handle: 0x{hWnd:X}");
Logger.LogInfo("[SHOWWINDOW] Adjusting window size");
AdjustWindowSizeToContent(); AdjustWindowSizeToContent();
Logger.LogInfo("[SHOWWINDOW] Repositioning window");
if (_appWindow != null) if (_appWindow != null)
{ {
PositionWindowAtBottomRight(_appWindow); PositionWindowAtBottomRight(_appWindow);
Logger.LogInfo("[SHOWWINDOW] Window repositioned");
} }
else else
{ {
Logger.LogWarning("[SHOWWINDOW] _appWindow is null, skipping reposition"); Logger.LogWarning("AppWindow is null, skipping window repositioning");
} }
Logger.LogInfo("[SHOWWINDOW] Setting opacity to 0 for animation");
RootGrid.Opacity = 0; RootGrid.Opacity = 0;
Logger.LogInfo("[SHOWWINDOW] Calling this.Activate()");
this.Activate(); this.Activate();
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelper.ShowWindow");
WindowHelper.ShowWindow(hWnd, true); WindowHelper.ShowWindow(hWnd, true);
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelpers.BringToForeground");
WindowHelpers.BringToForeground(hWnd); WindowHelpers.BringToForeground(hWnd);
Logger.LogInfo("[SHOWWINDOW] Checking for animation storyboard");
if (RootGrid.Resources.ContainsKey("SlideInStoryboard")) if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
{ {
Logger.LogInfo("[SHOWWINDOW] Starting SlideInStoryboard animation");
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard; var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
slideInStoryboard?.Begin(); slideInStoryboard?.Begin();
} }
else else
{ {
Logger.LogWarning("[SHOWWINDOW] SlideInStoryboard not found, setting opacity=1"); Logger.LogWarning("SlideInStoryboard not found, window will appear without animation");
RootGrid.Opacity = 1; RootGrid.Opacity = 1;
} }
Logger.LogInfo("[SHOWWINDOW] Verifying window visibility");
bool isVisible = IsWindowVisible(); bool isVisible = IsWindowVisible();
Logger.LogInfo($"[SHOWWINDOW] IsWindowVisible result: {isVisible}");
if (!isVisible) if (!isVisible)
{ {
Logger.LogError("[SHOWWINDOW] Window not visible after show, forcing visibility"); Logger.LogError("Window not visible after show attempt, forcing visibility");
RootGrid.Opacity = 1; RootGrid.Opacity = 1;
this.Activate(); this.Activate();
WindowHelpers.BringToForeground(hWnd); WindowHelpers.BringToForeground(hWnd);
} }
Logger.LogInfo("[SHOWWINDOW] Method completed successfully");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"[SHOWWINDOW] Exception: {ex.GetType().Name}"); Logger.LogError($"Failed to show window: {ex.Message}");
Logger.LogError($"[SHOWWINDOW] Exception message: {ex.Message}");
Logger.LogError($"[SHOWWINDOW] Stack trace: {ex.StackTrace}");
throw; throw;
} }
} }
@@ -346,35 +316,28 @@ namespace PowerDisplay
/// </summary> /// </summary>
public void ToggleWindow() public void ToggleWindow()
{ {
Logger.LogInfo("[TOGGLEWINDOW] Method entry");
try try
{ {
bool isVisible = IsWindowVisible(); bool isVisible = IsWindowVisible();
Logger.LogInfo($"[TOGGLEWINDOW] Current visibility: {isVisible}");
if (isVisible) if (isVisible)
{ {
Logger.LogInfo("[TOGGLEWINDOW] Window is visible, hiding");
HideWindow(); HideWindow();
} }
else else
{ {
Logger.LogInfo("[TOGGLEWINDOW] Window is hidden, showing");
ShowWindow(); ShowWindow();
} }
Logger.LogInfo("[TOGGLEWINDOW] Method completed");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"[TOGGLEWINDOW] Exception: {ex.Message}"); Logger.LogError($"Failed to toggle window: {ex.Message}");
throw; throw;
} }
} }
private async void OnUIRefreshRequested(object? sender, EventArgs e) private async void OnUIRefreshRequested(object? sender, EventArgs e)
{ {
Logger.LogInfo("UI refresh requested due to settings change");
await _viewModel.ReloadMonitorSettingsAsync(); await _viewModel.ReloadMonitorSettingsAsync();
// Adjust window size after settings are reloaded (no delay needed!) // Adjust window size after settings are reloaded (no delay needed!)
@@ -415,7 +378,7 @@ namespace PowerDisplay
} }
/// <summary> /// <summary>
/// 快速关闭窗口,跳过动画和复杂清理 /// Fast shutdown: skip animations and complex cleanup
/// </summary> /// </summary>
public void FastShutdown() public void FastShutdown()
{ {
@@ -423,25 +386,25 @@ namespace PowerDisplay
{ {
_isExiting = true; _isExiting = true;
// 快速清理 ViewModel // Quick cleanup of ViewModel
if (_viewModel != null) if (_viewModel != null)
{ {
// 取消事件订阅 // Unsubscribe from events
_viewModel.UIRefreshRequested -= OnUIRefreshRequested; _viewModel.UIRefreshRequested -= OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged; _viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
_viewModel.PropertyChanged -= OnViewModelPropertyChanged; _viewModel.PropertyChanged -= OnViewModelPropertyChanged;
// 立即释放 // Dispose immediately
_viewModel.Dispose(); _viewModel.Dispose();
} }
// 直接关闭窗口,不等待动画 // Close window directly without animations
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowHelper.ShowWindow(hWnd, false); WindowHelper.ShowWindow(hWnd, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
// 忽略清理错误,确保能够关闭 // Ignore cleanup errors to ensure shutdown
Logger.LogWarning($"FastShutdown error: {ex.Message}"); Logger.LogWarning($"FastShutdown error: {ex.Message}");
} }
} }
@@ -450,21 +413,21 @@ namespace PowerDisplay
{ {
try try
{ {
// 使用快速关闭 // Use fast shutdown
FastShutdown(); FastShutdown();
// 直接调用应用程序快速退出 // Call application shutdown directly
if (Application.Current is App app) if (Application.Current is App app)
{ {
app.Shutdown(); app.Shutdown();
} }
// 确保立即退出 // Ensure immediate exit
Environment.Exit(0); Environment.Exit(0);
} }
catch (Exception ex) catch (Exception ex)
{ {
// 确保能够退出 // Ensure exit even on error
Logger.LogError($"ExitApplication error: {ex.Message}"); Logger.LogError($"ExitApplication error: {ex.Message}");
Environment.Exit(0); Environment.Exit(0);
} }
@@ -618,7 +581,10 @@ namespace PowerDisplay
} }
} }
private void SetupWindow() /// <summary>
/// Configure window properties (synchronous, no data dependency)
/// </summary>
private void ConfigureWindow()
{ {
try try
{ {
@@ -712,7 +678,7 @@ namespace PowerDisplay
catch (Exception ex) catch (Exception ex)
{ {
// Ignore window setup errors // Ignore window setup errors
Logger.LogWarning($"Window setup error: {ex.Message}"); Logger.LogWarning($"Window configuration error: {ex.Message}");
} }
} }
@@ -743,7 +709,6 @@ namespace PowerDisplay
var currentSize = _appWindow.Size; var currentSize = _appWindow.Size;
if (Math.Abs(currentSize.Height - scaledHeight) > 1) if (Math.Abs(currentSize.Height - scaledHeight) > 1)
{ {
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight }); _appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
// Update clip region to match new window size // Update clip region to match new window size
@@ -800,10 +765,9 @@ namespace PowerDisplay
appWindow.Move(new PointInt32 { X = x, Y = y }); appWindow.Move(new PointInt32 { X = x, Y = y });
} }
} }
catch (Exception ex) catch (Exception)
{ {
// Ignore errors when positioning window // Ignore errors when positioning window
Logger.LogDebug($"Failed to position window: {ex.Message}");
} }
} }
@@ -857,19 +821,15 @@ namespace PowerDisplay
{ {
case "Brightness": case "Brightness":
monitorVm.Brightness = finalValue; monitorVm.Brightness = finalValue;
Logger.LogDebug($"[UI] Brightness drag completed: {finalValue}");
break; break;
case "ColorTemperature": case "ColorTemperature":
monitorVm.ColorTemperaturePercent = finalValue; monitorVm.ColorTemperaturePercent = finalValue;
Logger.LogDebug($"[UI] ColorTemperature drag completed: {finalValue}%");
break; break;
case "Contrast": case "Contrast":
monitorVm.ContrastPercent = finalValue; monitorVm.ContrastPercent = finalValue;
Logger.LogDebug($"[UI] Contrast drag completed: {finalValue}%");
break; break;
case "Volume": case "Volume":
monitorVm.Volume = finalValue; monitorVm.Volume = finalValue;
Logger.LogDebug($"[UI] Volume drag completed: {finalValue}");
break; break;
} }
} }

View File

@@ -1,343 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "pch.h"
#include "PowerDisplayProcessManager.h"
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <common/interop/shared_constants.h>
#include <atlstr.h>
#include <format>
namespace
{
/// <summary>
/// Generate a pipe name with UUID suffix
/// </summary>
std::optional<std::wstring> get_pipe_uuid()
{
UUID temp_uuid;
wchar_t* uuid_chars = nullptr;
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
{
const auto val = get_last_error_message(GetLastError());
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
return std::nullopt;
}
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
{
const auto val = get_last_error_message(GetLastError());
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
return std::nullopt;
}
const auto uuid_str = std::wstring(uuid_chars);
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
return uuid_str;
}
}
PowerDisplayProcessManager::~PowerDisplayProcessManager()
{
stop();
}
void PowerDisplayProcessManager::start()
{
m_enabled = true;
submit_task([this]() { refresh(); });
}
void PowerDisplayProcessManager::stop()
{
m_enabled = false;
submit_task([this]() { refresh(); });
}
void PowerDisplayProcessManager::send_message_to_powerdisplay(const std::wstring& message)
{
submit_task([this, message]() {
if (m_write_pipe)
{
try
{
const auto formatted = std::format(L"{}\r\n", message);
// Match WinUI side which reads the pipe using UTF-16 (Encoding.Unicode)
const CString payload(formatted.c_str());
const DWORD bytes_to_write = static_cast<DWORD>(payload.GetLength() * sizeof(wchar_t));
DWORD bytes_written = 0;
if (FAILED(m_write_pipe->Write(payload, bytes_to_write, &bytes_written)))
{
Logger::error(L"Failed to write message to PowerDisplay pipe");
}
else
{
Logger::trace(L"Sent message to PowerDisplay: {}", message);
}
}
catch (...)
{
Logger::error(L"Exception while sending message to PowerDisplay");
}
}
else
{
Logger::warn(L"Cannot send message to PowerDisplay: pipe not connected");
}
});
}
void PowerDisplayProcessManager::submit_task(std::function<void()> task)
{
m_thread_executor.submit(OnThreadExecutor::task_t{ task });
}
bool PowerDisplayProcessManager::is_process_running() const
{
return m_hProcess != nullptr && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void PowerDisplayProcessManager::terminate_process()
{
// Terminate process if still running
if (m_hProcess != nullptr)
{
// Check if process is still running
if (WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
{
Logger::trace(L"Process still running, calling TerminateProcess");
// Force terminate the process
if (TerminateProcess(m_hProcess, 1))
{
// Wait a bit to ensure process is terminated
DWORD wait_result = WaitForSingleObject(m_hProcess, 1000);
if (wait_result == WAIT_OBJECT_0)
{
Logger::trace(L"PowerDisplay process successfully terminated");
}
else
{
Logger::error(L"TerminateProcess succeeded but process did not exit within timeout");
}
}
else
{
Logger::error(L"TerminateProcess failed: {}", get_last_error_or_default(GetLastError()));
}
}
else
{
Logger::trace(L"PowerDisplay process already exited gracefully");
}
// Clean up process handle
CloseHandle(m_hProcess);
m_hProcess = nullptr;
}
// Close pipe after process is terminated
m_write_pipe.reset();
Logger::trace(L"PowerDisplay process cleanup complete");
}
HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid)
{
const unsigned long powertoys_pid = GetCurrentProcessId();
// Pass both runner PID and pipe UUID to PowerDisplay.exe
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_uuid);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
Logger::trace(L"Successfully started PowerDisplay process with UUID: {}", pipe_uuid);
terminate_process(); // Clean up old process if any
m_hProcess = sei.hProcess;
return S_OK;
}
else
{
Logger::error(L"PowerDisplay process failed to start. {}", get_last_error_or_default(GetLastError()));
return E_FAIL;
}
}
HRESULT PowerDisplayProcessManager::start_command_pipe(const std::wstring& pipe_uuid)
{
const constexpr DWORD BUFSIZE = 4096 * 4;
const constexpr DWORD client_timeout_millis = 5000;
// Create single-direction pipe: ModuleInterface writes commands to PowerDisplay
const std::wstring pipe_name = std::format(L"\\\\.\\pipe\\powertoys_powerdisplay_{}", pipe_uuid);
HANDLE hWritePipe = CreateNamedPipe(
pipe_name.c_str(),
PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1, // max instances
BUFSIZE, // out buffer size
0, // in buffer size (not used for outbound)
0, // client timeout
NULL // default security
);
if (hWritePipe == NULL || hWritePipe == INVALID_HANDLE_VALUE)
{
Logger::error(L"Error creating pipe for PowerDisplay");
return E_FAIL;
}
// Create overlapped event for waiting for client to connect
OVERLAPPED overlapped = { 0 };
overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!overlapped.hEvent)
{
Logger::error(L"Error creating overlapped event for PowerDisplay pipe");
CloseHandle(hWritePipe);
return E_FAIL;
}
// Connect pipe
if (!ConnectNamedPipe(hWritePipe, &overlapped))
{
const auto lastError = GetLastError();
if (lastError != ERROR_IO_PENDING && lastError != ERROR_PIPE_CONNECTED)
{
Logger::error(L"Error connecting pipe");
CloseHandle(overlapped.hEvent);
CloseHandle(hWritePipe);
return E_FAIL;
}
}
// Wait for pipe to connect (with timeout)
DWORD wait_result = WaitForSingleObject(overlapped.hEvent, client_timeout_millis);
CloseHandle(overlapped.hEvent);
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_TIMEOUT)
{
// Check if actually connected
DWORD bytes_transferred = 0;
if (GetOverlappedResult(hWritePipe, &overlapped, &bytes_transferred, FALSE) || GetLastError() == ERROR_PIPE_CONNECTED)
{
m_write_pipe = std::make_unique<CAtlFile>(hWritePipe);
Logger::trace(L"PowerDisplay pipe connected successfully: {}", pipe_name);
return S_OK;
}
}
Logger::error(L"Timeout waiting for PowerDisplay to connect to pipe");
CloseHandle(hWritePipe);
return E_FAIL;
}
void PowerDisplayProcessManager::refresh()
{
if (m_enabled == is_process_running())
{
// Already in correct state
return;
}
if (m_enabled)
{
// Start PowerDisplay process
Logger::trace(L"Starting PowerDisplay process");
const auto pipe_uuid = get_pipe_uuid();
if (!pipe_uuid)
{
Logger::error(L"Failed to generate pipe UUID");
return;
}
// FIX BUG #1: Start process FIRST, then create pipes
// This ensures PowerDisplay.exe is running when pipes try to connect
if (start_process(pipe_uuid.value()) != S_OK)
{
Logger::error(L"Failed to start PowerDisplay process");
return;
}
// Now create pipes and wait for PowerDisplay to connect
if (start_command_pipe(pipe_uuid.value()) != S_OK)
{
Logger::error(L"Failed to initialize command pipes, terminating process");
terminate_process();
}
}
else
{
// Stop PowerDisplay process
Logger::trace(L"Stopping PowerDisplay process");
// Send terminate message synchronously (not through thread executor)
// This ensures the message is sent before we wait for process exit
if (m_write_pipe)
{
try
{
const auto message = L"{\"action\":\"terminate\"}";
const auto formatted = std::format(L"{}\r\n", message);
// Match WinUI side which reads the pipe using UTF-16 (Encoding.Unicode)
const CString payload(formatted.c_str());
const DWORD bytes_to_write = static_cast<DWORD>(payload.GetLength() * sizeof(wchar_t));
DWORD bytes_written = 0;
if (SUCCEEDED(m_write_pipe->Write(payload, bytes_to_write, &bytes_written)))
{
Logger::trace(L"Sent terminate message to PowerDisplay");
}
else
{
Logger::warn(L"Failed to send terminate message to PowerDisplay");
}
}
catch (...)
{
Logger::warn(L"Exception while sending terminate message to PowerDisplay");
}
}
else
{
Logger::warn(L"Cannot send terminate message: pipe not connected");
}
// Wait for graceful exit (use longer timeout like AdvancedPaste)
if (m_hProcess != nullptr)
{
Logger::trace(L"Waiting for PowerDisplay process to exit gracefully");
DWORD wait_result = WaitForSingleObject(m_hProcess, 5000);
if (wait_result == WAIT_OBJECT_0)
{
Logger::trace(L"PowerDisplay process exited gracefully");
}
else if (wait_result == WAIT_TIMEOUT)
{
Logger::warn(L"PowerDisplay process failed to exit within timeout, will force terminate");
}
else
{
Logger::error(L"WaitForSingleObject failed with error: {}", get_last_error_or_default(GetLastError()));
}
}
// Clean up (will force terminate if still running)
terminate_process();
}
}

View File

@@ -1,73 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <functional>
#include <memory>
#include <thread>
#include <atlfile.h>
#include <common/utils/OnThreadExecutor.h>
/// <summary>
/// Manages PowerDisplay.exe process lifecycle and IPC communication
/// </summary>
class PowerDisplayProcessManager
{
private:
HANDLE m_hProcess = nullptr;
std::unique_ptr<CAtlFile> m_write_pipe;
OnThreadExecutor m_thread_executor;
bool m_enabled = false;
public:
PowerDisplayProcessManager() = default;
~PowerDisplayProcessManager();
/// <summary>
/// Start PowerDisplay.exe process
/// </summary>
void start();
/// <summary>
/// Stop PowerDisplay.exe process
/// </summary>
void stop();
/// <summary>
/// Send message to PowerDisplay.exe
/// </summary>
void send_message_to_powerdisplay(const std::wstring& message);
private:
/// <summary>
/// Submit task to thread executor
/// </summary>
void submit_task(std::function<void()> task);
/// <summary>
/// Check if PowerDisplay.exe is running
/// </summary>
bool is_process_running() const;
/// <summary>
/// Terminate PowerDisplay.exe process
/// </summary>
void terminate_process();
/// <summary>
/// Start PowerDisplay.exe with command line arguments
/// </summary>
HRESULT start_process(const std::wstring& pipe_uuid);
/// <summary>
/// Create named pipe for sending commands to PowerDisplay
/// </summary>
HRESULT start_command_pipe(const std::wstring& pipe_uuid);
/// <summary>
/// Refresh - start or stop process based on m_enabled state
/// </summary>
void refresh();
};