Fix some issues

This commit is contained in:
moooyo
2025-12-01 06:09:26 +08:00
parent 0bbfc8015a
commit 391f61d4ed
12 changed files with 212 additions and 54 deletions

View File

@@ -181,7 +181,7 @@ namespace PowerDisplay.Common.Drivers.DDC
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max)) if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max))
{ {
var presetName = VcpValueNames.GetFormattedName(0x14, (int)current); var presetName = VcpValueNames.GetFormattedName(0x14, (int)current);
Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: {presetName}"); Logger.LogDebug($"[{monitor.Id}] Color temperature via 0x14: {presetName}");
return new BrightnessInfo((int)current, 0, (int)max); return new BrightnessInfo((int)current, 0, (int)max);
} }
@@ -266,7 +266,7 @@ namespace PowerDisplay.Common.Drivers.DDC
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeInputSource, out uint current, out uint max)) if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeInputSource, out uint current, out uint max))
{ {
var sourceName = VcpValueNames.GetFormattedName(0x60, (int)current); var sourceName = VcpValueNames.GetFormattedName(0x60, (int)current);
Logger.LogInfo($"[{monitor.Id}] Input source via 0x60: {sourceName}"); Logger.LogDebug($"[{monitor.Id}] Input source via 0x60: {sourceName}");
return new BrightnessInfo((int)current, 0, (int)max); return new BrightnessInfo((int)current, 0, (int)max);
} }
@@ -322,7 +322,7 @@ namespace PowerDisplay.Common.Drivers.DDC
var verifyName = VcpValueNames.GetFormattedName(0x60, (int)verifyValue); var verifyName = VcpValueNames.GetFormattedName(0x60, (int)verifyValue);
if (verifyValue == (uint)inputSource) if (verifyValue == (uint)inputSource)
{ {
Logger.LogInfo($"[{monitor.Id}] Input source verified: {verifyName} (0x{verifyValue:X2})"); Logger.LogDebug($"[{monitor.Id}] Input source verified: {verifyName} (0x{verifyValue:X2})");
} }
else else
{ {
@@ -409,7 +409,7 @@ namespace PowerDisplay.Common.Drivers.DDC
if (!string.IsNullOrEmpty(capsString)) if (!string.IsNullOrEmpty(capsString))
{ {
Logger.LogInfo($"Got capabilities string (length: {capsString.Length})"); Logger.LogDebug($"Got capabilities string (length: {capsString.Length})");
return capsString; return capsString;
} }
} }

View File

@@ -16,8 +16,21 @@ namespace PowerDisplay.Common.Services
/// Centralized service for managing PowerDisplay profiles storage and retrieval. /// Centralized service for managing PowerDisplay profiles storage and retrieval.
/// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules. /// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules.
/// Thread-safe and AOT-compatible. /// Thread-safe and AOT-compatible.
/// Implements <see cref="IProfileService"/> for dependency injection support.
/// </summary> /// </summary>
/// <remarks>
/// <para><b>Design Note:</b> This class provides both static and instance-based access patterns:</para>
/// <list type="bullet">
/// <item><description>Static methods: Direct access for backward compatibility with existing code. Thread-safe with internal locking.</description></item>
/// <item><description>Instance methods (via <see cref="IProfileService"/>): For dependency injection consumers. Delegates to static methods internally.</description></item>
/// </list>
/// <para><b>Usage Guidelines:</b></para>
/// <list type="bullet">
/// <item><description>New code should prefer DI: <c>serviceProvider.GetService&lt;IProfileService&gt;()</c></description></item>
/// <item><description>Or use the singleton: <c>ProfileService.Instance.LoadProfiles()</c></description></item>
/// <item><description>Static methods remain available for backward compatibility</description></item>
/// </list>
/// <para>All access patterns are thread-safe and share the same underlying lock.</para>
/// </remarks>
public class ProfileService : IProfileService public class ProfileService : IProfileService
{ {
private const string LogPrefix = "[ProfileService]"; private const string LogPrefix = "[ProfileService]";

View File

@@ -0,0 +1,47 @@
// 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.Threading;
using ManagedCommon;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for Windows named event operations.
/// Provides unified event signaling with consistent error handling and logging.
/// </summary>
public static class EventHelper
{
/// <summary>
/// Signals a named event. Creates the event if it doesn't exist.
/// </summary>
/// <param name="eventName">The name of the event to signal.</param>
/// <returns>True if the event was signaled successfully, false otherwise.</returns>
public static bool SignalEvent(string eventName)
{
if (string.IsNullOrEmpty(eventName))
{
Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name");
return false;
}
try
{
using var eventHandle = new EventWaitHandle(
false,
EventResetMode.AutoReset,
eventName);
eventHandle.Set();
Logger.LogDebug($"[EventHelper] Signaled event: {eventName}");
return true;
}
catch (Exception ex)
{
Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}");
return false;
}
}
}
}

View File

@@ -34,5 +34,60 @@ namespace PowerDisplay.Configuration
/// </summary> /// </summary>
public const string ExternalMonitorGlyph = "\uE7F4"; public const string ExternalMonitorGlyph = "\uE7F4";
} }
/// <summary>
/// DDC/CI protocol constants
/// </summary>
public static class Ddc
{
/// <summary>
/// Retry delay between DDC/CI operations in milliseconds
/// </summary>
public const int RetryDelayMs = 100;
/// <summary>
/// Maximum number of retries for DDC/CI operations
/// </summary>
public const int MaxRetries = 3;
/// <summary>
/// Timeout for getting monitor capabilities in milliseconds
/// </summary>
public const int CapabilitiesTimeoutMs = 5000;
// VCP Codes
/// <summary>VCP code for Brightness (0x10)</summary>
public const byte VcpBrightness = 0x10;
/// <summary>VCP code for Contrast (0x12)</summary>
public const byte VcpContrast = 0x12;
/// <summary>VCP code for Select Color Preset / Color Temperature (0x14)</summary>
public const byte VcpColorTemperature = 0x14;
/// <summary>VCP code for Input Source (0x60)</summary>
public const byte VcpInputSource = 0x60;
}
/// <summary>
/// Process synchronization constants
/// </summary>
public static class Process
{
/// <summary>
/// Timeout for waiting for process ready signal in milliseconds
/// </summary>
public const int StartupTimeoutMs = 5000;
/// <summary>
/// Polling interval when waiting for process ready in milliseconds
/// </summary>
public const int ReadyPollIntervalMs = 100;
/// <summary>
/// Fallback delay when event-based synchronization fails in milliseconds
/// </summary>
public const int FallbackDelayMs = 500;
}
} }
} }

View File

@@ -26,6 +26,12 @@ namespace PowerDisplay
public partial class App : Application public partial class App : Application
#pragma warning restore CA1001 #pragma warning restore CA1001
{ {
/// <summary>
/// Event name for signaling that PowerDisplay process is ready.
/// Must match the constant in C++ PowerDisplayModuleInterface.
/// </summary>
private const string ProcessReadyEventName = "Local\\PowerToys_PowerDisplay_Ready";
private readonly ISettingsUtils _settingsUtils = new SettingsUtils(); private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
private Window? _mainWindow; private Window? _mainWindow;
private int _powerToysRunnerPid; private int _powerToysRunnerPid;
@@ -106,6 +112,10 @@ namespace PowerDisplay
RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature"); RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature");
RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile"); RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile");
// Signal that process is ready to receive events
// This allows the C++ module to wait for initialization instead of using hardcoded Sleep
SignalProcessReady();
// Monitor Runner process (backup exit mechanism) // Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0) if (_powerToysRunnerPid > 0)
{ {
@@ -354,5 +364,26 @@ namespace PowerDisplay
_trayIconService?.Destroy(); _trayIconService?.Destroy();
Environment.Exit(0); Environment.Exit(0);
} }
/// <summary>
/// Signal that PowerDisplay process is ready to receive events.
/// Uses a ManualReset event so the C++ module can wait on it.
/// </summary>
private static void SignalProcessReady()
{
try
{
using var readyEvent = new EventWaitHandle(
false,
EventResetMode.ManualReset,
ProcessReadyEventName);
readyEvent.Set();
Logger.LogInfo("Signaled process ready event");
}
catch (Exception ex)
{
Logger.LogError($"Failed to signal process ready event: {ex.Message}");
}
}
} }
} }

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
@@ -19,7 +20,7 @@ namespace PowerDisplay.ViewModels;
/// </summary> /// </summary>
public partial class MainViewModel public partial class MainViewModel
{ {
private async Task InitializeAsync() private async Task InitializeAsync(CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -27,7 +28,7 @@ public partial class MainViewModel
IsScanning = true; IsScanning = true;
// Discover monitors // Discover monitors
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token); var monitors = await _monitorManager.DiscoverMonitorsAsync(cancellationToken);
// Update UI on the dispatcher thread // Update UI on the dispatcher thread
_dispatcherQueue.TryEnqueue(() => _dispatcherQueue.TryEnqueue(() =>

View File

@@ -10,6 +10,7 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models; using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services; using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerDisplay.Serialization; using PowerDisplay.Serialization;
using PowerToys.Interop; using PowerToys.Interop;
@@ -621,21 +622,8 @@ public partial class MainViewModel
/// </summary> /// </summary>
private void SignalMonitorsRefreshEvent() private void SignalMonitorsRefreshEvent()
{ {
try EventHelper.SignalEvent(Constants.RefreshPowerDisplayMonitorsEvent());
{ Logger.LogInfo("Signaled refresh monitors event to Settings UI");
using (var eventHandle = new System.Threading.EventWaitHandle(
false,
System.Threading.EventResetMode.AutoReset,
Constants.RefreshPowerDisplayMonitorsEvent()))
{
eventHandle.Set();
Logger.LogInfo("Signaled refresh monitors event to Settings UI");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to signal refresh monitors event: {ex.Message}");
}
} }
/// <summary> /// <summary>

View File

@@ -80,7 +80,7 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
LoadProfiles(); LoadProfiles();
// Start initial discovery // Start initial discovery
_ = InitializeAsync(); _ = InitializeAsync(_cancellationTokenSource.Token);
} }
public ObservableCollection<MonitorViewModel> Monitors public ObservableCollection<MonitorViewModel> Monitors

View File

@@ -404,6 +404,13 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param> /// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
public async Task SetRotationAsync(int orientation) public async Task SetRotationAsync(int orientation)
{ {
// Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°)
if (orientation < 0 || orientation > 3)
{
Logger.LogWarning($"[{HardwareId}] Invalid rotation value: {orientation}. Must be 0-3.");
return;
}
// If already at this orientation, do nothing // If already at this orientation, do nothing
if (CurrentRotation == orientation) if (CurrentRotation == orientation)
{ {

View File

@@ -4,4 +4,9 @@ namespace PowerDisplayConstants
{ {
// Name of the powertoy module. // Name of the powertoy module.
inline const std::wstring ModuleKey = L"PowerDisplay"; inline const std::wstring ModuleKey = L"PowerDisplay";
// Process synchronization constants
inline const wchar_t* ProcessReadyEventName = L"Local\\PowerToys_PowerDisplay_Ready";
constexpr DWORD ProcessReadyTimeoutMs = 5000;
constexpr DWORD FallbackDelayMs = 500;
} }

View File

@@ -185,6 +185,38 @@ private:
} }
} }
// Helper method to wait for PowerDisplay.exe process to be ready
// Uses a named event for precise synchronization instead of hardcoded Sleep
void wait_for_process_ready()
{
HANDLE readyEvent = OpenEventW(SYNCHRONIZE, FALSE, PowerDisplayConstants::ProcessReadyEventName);
if (readyEvent)
{
Logger::trace(L"Waiting for PowerDisplay process ready signal...");
DWORD waitResult = WaitForSingleObject(readyEvent, PowerDisplayConstants::ProcessReadyTimeoutMs);
CloseHandle(readyEvent);
if (waitResult == WAIT_OBJECT_0)
{
Logger::trace(L"PowerDisplay process ready signal received");
}
else if (waitResult == WAIT_TIMEOUT)
{
Logger::warn(L"PowerDisplay process ready timeout after {}ms", PowerDisplayConstants::ProcessReadyTimeoutMs);
}
else
{
Logger::warn(L"WaitForSingleObject failed with error: {}", GetLastError());
}
}
else
{
// Fallback: if cannot open event, use a shorter delay
Logger::warn(L"Could not open PowerDisplay ready event, using fallback delay");
Sleep(PowerDisplayConstants::FallbackDelayMs);
}
}
public: public:
PowerDisplayModule() PowerDisplayModule()
{ {
@@ -332,9 +364,8 @@ public:
{ {
Logger::trace(L"PowerDisplay process not running, launching before applying color temperature"); Logger::trace(L"PowerDisplay process not running, launching before applying color temperature");
launch_process(); launch_process();
// Wait for process to initialize and register event listeners // Wait for process to signal ready
// This prevents race condition where event is set before process is ready wait_for_process_ready();
Sleep(1000);
} }
if (m_hApplyColorTemperatureEvent) if (m_hApplyColorTemperatureEvent)
@@ -364,8 +395,8 @@ public:
{ {
Logger::trace(L"PowerDisplay process not running, launching before applying profile"); Logger::trace(L"PowerDisplay process not running, launching before applying profile");
launch_process(); launch_process();
// Wait for process to initialize and register event listeners // Wait for process to signal ready
Sleep(1000); wait_for_process_ready();
} }
if (m_hApplyProfileEvent) if (m_hApplyProfileEvent)

View File

@@ -142,11 +142,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
// Explicitly signal PowerDisplay to refresh tray icon // Explicitly signal PowerDisplay to refresh tray icon
// This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues // This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues
using var eventHandle = new System.Threading.EventWaitHandle( EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent());
false,
System.Threading.EventResetMode.AutoReset,
Constants.SettingsUpdatedPowerDisplayEvent());
eventHandle.Set();
Logger.LogInfo($"ShowSystemTrayIcon changed to {value}, signaled SettingsUpdatedPowerDisplayEvent"); Logger.LogInfo($"ShowSystemTrayIcon changed to {value}, signaled SettingsUpdatedPowerDisplayEvent");
} }
} }
@@ -396,19 +392,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
/// </summary> /// </summary>
private void SignalSettingsUpdated() private void SignalSettingsUpdated()
{ {
try EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent());
{ Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change");
using var eventHandle = new System.Threading.EventWaitHandle(
false,
System.Threading.EventResetMode.AutoReset,
Constants.SettingsUpdatedPowerDisplayEvent());
eventHandle.Set();
Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change");
}
catch (Exception ex)
{
Logger.LogError($"Failed to signal SettingsUpdatedPowerDisplayEvent: {ex.Message}");
}
} }
public void Launch() public void Launch()
@@ -513,7 +498,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
for (int i = Monitors.Count - 1; i >= 0; i--) for (int i = Monitors.Count - 1; i >= 0; i--)
{ {
var existingMonitor = Monitors[i]; var existingMonitor = Monitors[i];
if (updatedMonitorsDict.TryGetValue(existingMonitor.InternalName, out var updatedMonitor)) if (updatedMonitorsDict.TryGetValue(existingMonitor.InternalName, out var updatedMonitor)
&& updatedMonitor != null)
{ {
// Monitor still exists - update its properties in place // Monitor still exists - update its properties in place
Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.InternalName}"); Logger.LogInfo($"[ReloadMonitors] Updating existing monitor: {existingMonitor.InternalName}");
@@ -661,13 +647,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
profile.Name)); profile.Name));
// Signal PowerDisplay to apply profile // Signal PowerDisplay to apply profile
using (var eventHandle = new System.Threading.EventWaitHandle( EventHelper.SignalEvent(Constants.ApplyProfilePowerDisplayEvent());
false,
System.Threading.EventResetMode.AutoReset,
Constants.ApplyProfilePowerDisplayEvent()))
{
eventHandle.Set();
}
Logger.LogInfo($"Profile '{profile.Name}' applied successfully"); Logger.LogInfo($"Profile '{profile.Name}' applied successfully");
} }