mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
Refactor: use Polly, ConcurrentDictionary, unify VCP parsing
- Replace custom retry logic with Polly.Core resilience pipelines for DDC/CI operations - Remove LockedDictionary and RetryHelper; use ConcurrentDictionary for thread safety - Add MonitorFeatureHelper to centralize VCP feature parsing - Simplify monitor state management and update code for clarity - Add Polly.Core dependency and update documentation - Remove obsolete helper files
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="Polly.Core" Version="8.6.5" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
|
||||
@@ -1587,6 +1587,7 @@ SOFTWARE.
|
||||
- NLog.Extensions.Logging
|
||||
- NLog.Schema
|
||||
- OpenAI
|
||||
- Polly.Core
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using PowerDisplay.Common.Interfaces;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
@@ -45,6 +47,42 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
private const int RetryDelayMs = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Retry pipeline for getting capabilities string length (3 retries).
|
||||
/// </summary>
|
||||
private static readonly ResiliencePipeline<uint> CapabilitiesLengthRetryPipeline =
|
||||
new ResiliencePipelineBuilder<uint>()
|
||||
.AddRetry(new RetryStrategyOptions<uint>
|
||||
{
|
||||
MaxRetryAttempts = 2, // 2 retries = 3 total attempts
|
||||
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
|
||||
ShouldHandle = new PredicateBuilder<uint>().HandleResult(len => len == 0),
|
||||
OnRetry = static args =>
|
||||
{
|
||||
Logger.LogWarning($"[Retry] GetCapabilitiesStringLength returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
|
||||
return default;
|
||||
},
|
||||
})
|
||||
.Build();
|
||||
|
||||
/// <summary>
|
||||
/// Retry pipeline for getting capabilities string (5 retries).
|
||||
/// </summary>
|
||||
private static readonly ResiliencePipeline<string?> CapabilitiesStringRetryPipeline =
|
||||
new ResiliencePipelineBuilder<string?>()
|
||||
.AddRetry(new RetryStrategyOptions<string?>
|
||||
{
|
||||
MaxRetryAttempts = 4, // 4 retries = 5 total attempts
|
||||
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
|
||||
ShouldHandle = new PredicateBuilder<string?>().HandleResult(static str => string.IsNullOrEmpty(str)),
|
||||
OnRetry = static args =>
|
||||
{
|
||||
Logger.LogWarning($"[Retry] GetCapabilitiesString returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
|
||||
return default;
|
||||
},
|
||||
})
|
||||
.Build();
|
||||
|
||||
private readonly PhysicalMonitorHandleManager _handleManager = new();
|
||||
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
||||
|
||||
@@ -142,39 +180,33 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
try
|
||||
{
|
||||
// Step 1: Get capabilities string length with retry
|
||||
var length = RetryHelper.ExecuteWithRetry(
|
||||
() =>
|
||||
var length = CapabilitiesLengthRetryPipeline.Execute(() =>
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out uint len) && len > 0)
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out uint len) && len > 0)
|
||||
{
|
||||
return len;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
return 0u;
|
||||
},
|
||||
len => len > 0,
|
||||
maxRetries: 3,
|
||||
delayMs: RetryDelayMs,
|
||||
operationName: "GetCapabilitiesStringLength");
|
||||
return 0u;
|
||||
});
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
Logger.LogWarning("[Retry] GetCapabilitiesStringLength failed after 3 attempts");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Step 2: Get actual capabilities string with retry
|
||||
var capsString = RetryHelper.ExecuteWithRetry(
|
||||
() => TryGetCapabilitiesString(monitor.Handle, length),
|
||||
str => !string.IsNullOrEmpty(str),
|
||||
maxRetries: 5,
|
||||
delayMs: RetryDelayMs,
|
||||
operationName: "GetCapabilitiesString");
|
||||
var capsString = CapabilitiesStringRetryPipeline.Execute(
|
||||
() => TryGetCapabilitiesString(monitor.Handle, length));
|
||||
|
||||
if (!string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
Logger.LogDebug($"Got capabilities string (length: {capsString.Length})");
|
||||
return capsString;
|
||||
}
|
||||
|
||||
Logger.LogWarning("[Retry] GetCapabilitiesString failed after 5 attempts");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Common.Utils;
|
||||
using static PowerDisplay.Common.Drivers.PInvoke;
|
||||
|
||||
namespace PowerDisplay.Common.Drivers.DDC
|
||||
@@ -16,7 +17,8 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||
{
|
||||
// Mapping: monitorId -> physical handle (thread-safe)
|
||||
private readonly LockedDictionary<string, IntPtr> _monitorIdToHandleMap = new();
|
||||
private readonly ConcurrentDictionary<string, IntPtr> _monitorIdToHandleMap = new();
|
||||
private readonly object _handleLock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
@@ -24,28 +26,28 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
|
||||
{
|
||||
_monitorIdToHandleMap.ExecuteWithLock(dict =>
|
||||
// Lock to ensure atomic update (cleanup + replace)
|
||||
lock (_handleLock)
|
||||
{
|
||||
// Clean up unused handles before updating
|
||||
CleanupUnusedHandles(dict, newHandleMap);
|
||||
CleanupUnusedHandles(newHandleMap);
|
||||
|
||||
// Update the device key map
|
||||
dict.Clear();
|
||||
_monitorIdToHandleMap.Clear();
|
||||
foreach (var kvp in newHandleMap)
|
||||
{
|
||||
dict[kvp.Key] = kvp.Value;
|
||||
_monitorIdToHandleMap[kvp.Key] = kvp.Value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up handles that are no longer in use.
|
||||
/// Called within ExecuteWithLock context with the internal dictionary.
|
||||
/// Optimized to O(n) using HashSet lookup instead of O(n*m) nested loops.
|
||||
/// Called within lock context. Optimized to O(n) using HashSet lookup.
|
||||
/// </summary>
|
||||
private void CleanupUnusedHandles(Dictionary<string, IntPtr> currentHandles, Dictionary<string, IntPtr> newHandles)
|
||||
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
|
||||
{
|
||||
if (currentHandles.Count == 0)
|
||||
if (_monitorIdToHandleMap.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -54,14 +56,9 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
var reusedHandles = new HashSet<IntPtr>(newHandles.Values);
|
||||
|
||||
// Find handles to destroy: in old map but not reused (O(n) with O(1) lookup)
|
||||
var handlesToDestroy = new List<IntPtr>();
|
||||
foreach (var oldHandle in currentHandles.Values)
|
||||
{
|
||||
if (oldHandle != IntPtr.Zero && !reusedHandles.Contains(oldHandle))
|
||||
{
|
||||
handlesToDestroy.Add(oldHandle);
|
||||
}
|
||||
}
|
||||
var handlesToDestroy = _monitorIdToHandleMap.Values
|
||||
.Where(h => h != IntPtr.Zero && !reusedHandles.Contains(h))
|
||||
.ToList();
|
||||
|
||||
// Destroy unused handles
|
||||
foreach (var handle in handlesToDestroy)
|
||||
@@ -86,7 +83,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
// Release all physical monitor handles - get snapshot to avoid holding lock during cleanup
|
||||
var handles = _monitorIdToHandleMap.GetValuesSnapshot();
|
||||
var handles = _monitorIdToHandleMap.Values.ToList();
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly.Core" />
|
||||
<PackageReference Include="WmiLight" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
@@ -23,7 +24,8 @@ namespace PowerDisplay.Common.Services
|
||||
public partial class MonitorStateManager : IDisposable
|
||||
{
|
||||
private readonly string _stateFilePath;
|
||||
private readonly LockedDictionary<string, MonitorState> _states = new();
|
||||
private readonly ConcurrentDictionary<string, MonitorState> _states = new();
|
||||
private readonly object _statesLock = new();
|
||||
private readonly SimpleDebouncer _saveDebouncer;
|
||||
|
||||
private bool _disposed;
|
||||
@@ -83,39 +85,35 @@ namespace PowerDisplay.Common.Services
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldSave = _states.ExecuteWithLock(dict =>
|
||||
var state = _states.GetOrAdd(hardwareId, _ => new MonitorState());
|
||||
|
||||
// Update the specific property
|
||||
bool shouldSave = true;
|
||||
switch (property)
|
||||
{
|
||||
// Get or create state entry using HardwareId
|
||||
if (!dict.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
state = new MonitorState();
|
||||
dict[hardwareId] = state;
|
||||
}
|
||||
|
||||
// Update the specific property
|
||||
switch (property)
|
||||
{
|
||||
case "Brightness":
|
||||
state.Brightness = value;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
state.ColorTemperatureVcp = value;
|
||||
break;
|
||||
case "Contrast":
|
||||
state.Contrast = value;
|
||||
break;
|
||||
case "Volume":
|
||||
state.Volume = value;
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
return false;
|
||||
}
|
||||
case "Brightness":
|
||||
state.Brightness = value;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
state.ColorTemperatureVcp = value;
|
||||
break;
|
||||
case "Contrast":
|
||||
state.Contrast = value;
|
||||
break;
|
||||
case "Volume":
|
||||
state.Volume = value;
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
shouldSave = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldSave)
|
||||
{
|
||||
// Mark dirty for flush on dispose
|
||||
_isDirty = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule debounced save (SimpleDebouncer handles cancellation of previous calls)
|
||||
if (shouldSave)
|
||||
@@ -141,7 +139,7 @@ namespace PowerDisplay.Common.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_states.TryGetValue(hardwareId, out var state) && state != null)
|
||||
if (_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
return (state.Brightness, state.ColorTemperatureVcp, state.Contrast, state.Volume);
|
||||
}
|
||||
@@ -167,23 +165,20 @@ namespace PowerDisplay.Common.Services
|
||||
|
||||
if (stateFile?.Monitors != null)
|
||||
{
|
||||
_states.ExecuteWithLock(dict =>
|
||||
foreach (var kvp in stateFile.Monitors)
|
||||
{
|
||||
foreach (var kvp in stateFile.Monitors)
|
||||
{
|
||||
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
|
||||
var entry = kvp.Value;
|
||||
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
|
||||
var entry = kvp.Value;
|
||||
|
||||
dict[monitorKey] = new MonitorState
|
||||
{
|
||||
Brightness = entry.Brightness,
|
||||
ColorTemperatureVcp = entry.ColorTemperatureVcp,
|
||||
Contrast = entry.Contrast,
|
||||
Volume = entry.Volume,
|
||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||
};
|
||||
}
|
||||
});
|
||||
_states[monitorKey] = new MonitorState
|
||||
{
|
||||
Brightness = entry.Brightness,
|
||||
ColorTemperatureVcp = entry.ColorTemperatureVcp,
|
||||
Contrast = entry.Contrast,
|
||||
Volume = entry.Volume,
|
||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||
};
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[State] Loaded state for {stateFile.Monitors.Count} monitors from {_stateFilePath}");
|
||||
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", stateFile.Monitors.Keys)}");
|
||||
@@ -258,24 +253,21 @@ namespace PowerDisplay.Common.Services
|
||||
LastUpdated = now,
|
||||
};
|
||||
|
||||
_states.ExecuteWithLock(dict =>
|
||||
foreach (var kvp in _states)
|
||||
{
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
var monitorId = kvp.Key;
|
||||
var state = kvp.Value;
|
||||
var monitorId = kvp.Key;
|
||||
var state = kvp.Value;
|
||||
|
||||
stateFile.Monitors[monitorId] = new MonitorStateEntry
|
||||
{
|
||||
Brightness = state.Brightness,
|
||||
ColorTemperatureVcp = state.ColorTemperatureVcp,
|
||||
Contrast = state.Contrast,
|
||||
Volume = state.Volume,
|
||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
});
|
||||
stateFile.Monitors[monitorId] = new MonitorStateEntry
|
||||
{
|
||||
Brightness = state.Brightness,
|
||||
ColorTemperatureVcp = state.ColorTemperatureVcp,
|
||||
Contrast = state.Contrast,
|
||||
Volume = state.Volume,
|
||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
|
||||
return (json, stateFile.Monitors.Count);
|
||||
|
||||
@@ -1,93 +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.Collections.Generic;
|
||||
|
||||
#pragma warning disable SA1649 // File name should match first type name (generic class name is valid)
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe dictionary wrapper that provides atomic operations with minimal locking overhead.
|
||||
/// Designed for scenarios where simple get/set operations are common but complex transactions are rare.
|
||||
/// For complex multi-step transactions, use <see cref="ExecuteWithLock"/> to ensure atomicity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
|
||||
public class LockedDictionary<TKey, TValue>
|
||||
where TKey : notnull
|
||||
{
|
||||
private readonly Dictionary<TKey, TValue> _dictionary = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the value associated with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to locate.</param>
|
||||
/// <param name="value">When this method returns, contains the value if found; otherwise, the default value.</param>
|
||||
/// <returns>True if the key was found; otherwise, false.</returns>
|
||||
public bool TryGetValue(TKey key, out TValue? value)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _dictionary.TryGetValue(key, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all key/value pairs from the dictionary.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_dictionary.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a snapshot of all values in the dictionary.
|
||||
/// Returns a copy to ensure thread safety.
|
||||
/// </summary>
|
||||
/// <returns>A list containing copies of all values.</returns>
|
||||
public List<TValue> GetValuesSnapshot()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new List<TValue>(_dictionary.Values);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action within the lock, providing the internal dictionary for complex operations.
|
||||
/// Use this for multi-step transactions that need to be atomic.
|
||||
/// WARNING: Do not store or return references to the dictionary outside the action.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute with the dictionary.</param>
|
||||
public void ExecuteWithLock(Action<Dictionary<TKey, TValue>> action)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
action(_dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a function within the lock, providing the internal dictionary for complex operations.
|
||||
/// Use this for multi-step transactions that need to be atomic and return a result.
|
||||
/// WARNING: Do not store or return references to the dictionary outside the function.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the result.</typeparam>
|
||||
/// <param name="func">The function to execute with the dictionary.</param>
|
||||
/// <returns>The result of the function.</returns>
|
||||
public TResult ExecuteWithLock<TResult>(Func<Dictionary<TKey, TValue>, TResult> func)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return func(_dictionary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using PowerDisplay.Common.Drivers;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified helper class for parsing monitor feature support from VCP capabilities.
|
||||
/// This eliminates duplicate VCP parsing logic across PowerDisplay.exe and Settings.UI.
|
||||
/// </summary>
|
||||
public static class MonitorFeatureHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of parsing monitor feature support from VCP capabilities
|
||||
/// </summary>
|
||||
public readonly struct FeatureSupportResult
|
||||
{
|
||||
public bool SupportsBrightness { get; init; }
|
||||
|
||||
public bool SupportsContrast { get; init; }
|
||||
|
||||
public bool SupportsColorTemperature { get; init; }
|
||||
|
||||
public bool SupportsVolume { get; init; }
|
||||
|
||||
public bool SupportsInputSource { get; init; }
|
||||
|
||||
public string CapabilitiesStatus { get; init; }
|
||||
|
||||
public static FeatureSupportResult Unavailable => new()
|
||||
{
|
||||
SupportsBrightness = false,
|
||||
SupportsContrast = false,
|
||||
SupportsColorTemperature = false,
|
||||
SupportsVolume = false,
|
||||
SupportsInputSource = false,
|
||||
CapabilitiesStatus = "unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse feature support from a list of VCP code strings.
|
||||
/// This is the single source of truth for determining monitor capabilities.
|
||||
/// </summary>
|
||||
/// <param name="vcpCodes">List of VCP codes as strings (e.g., "0x10", "10", "0x12")</param>
|
||||
/// <param name="capabilitiesRaw">Raw capabilities string, used to determine availability status</param>
|
||||
/// <returns>Feature support result</returns>
|
||||
public static FeatureSupportResult ParseFeatureSupport(IReadOnlyList<string>? vcpCodes, string? capabilitiesRaw)
|
||||
{
|
||||
// Check capabilities availability
|
||||
if (string.IsNullOrEmpty(capabilitiesRaw))
|
||||
{
|
||||
return FeatureSupportResult.Unavailable;
|
||||
}
|
||||
|
||||
// Convert all VCP codes to integers for comparison
|
||||
var vcpCodeInts = ParseVcpCodesToIntegers(vcpCodes);
|
||||
|
||||
// Determine feature support based on VCP codes
|
||||
return new FeatureSupportResult
|
||||
{
|
||||
SupportsBrightness = vcpCodeInts.Contains(NativeConstants.VcpCodeBrightness),
|
||||
SupportsContrast = vcpCodeInts.Contains(NativeConstants.VcpCodeContrast),
|
||||
SupportsColorTemperature = vcpCodeInts.Contains(NativeConstants.VcpCodeSelectColorPreset),
|
||||
SupportsVolume = vcpCodeInts.Contains(NativeConstants.VcpCodeVolume),
|
||||
SupportsInputSource = vcpCodeInts.Contains(NativeConstants.VcpCodeInputSource),
|
||||
CapabilitiesStatus = "available",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP codes from string list to integer set
|
||||
/// Handles both hex formats: "0x10" and "10"
|
||||
/// </summary>
|
||||
private static HashSet<int> ParseVcpCodesToIntegers(IReadOnlyList<string>? vcpCodes)
|
||||
{
|
||||
var result = new HashSet<int>();
|
||||
|
||||
if (vcpCodes == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var code in vcpCodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove "0x" prefix if present and parse as hex
|
||||
var cleanCode = code.Trim();
|
||||
if (cleanCode.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cleanCode = cleanCode.Substring(2);
|
||||
}
|
||||
|
||||
if (int.TryParse(cleanCode, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int codeInt))
|
||||
{
|
||||
result.Add(codeInt);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +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.Threading;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for executing operations with retry logic.
|
||||
/// Provides a unified retry pattern for DDC/CI operations that may fail intermittently.
|
||||
/// </summary>
|
||||
public static class RetryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Default delay between retry attempts in milliseconds.
|
||||
/// </summary>
|
||||
public const int DefaultRetryDelayMs = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Default maximum number of retry attempts.
|
||||
/// </summary>
|
||||
public const int DefaultMaxRetries = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Executes a synchronous operation with retry logic.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the operation.</typeparam>
|
||||
/// <param name="operation">The operation to execute.</param>
|
||||
/// <param name="isValid">Predicate to determine if the result is valid.</param>
|
||||
/// <param name="maxRetries">Maximum number of retry attempts (default: 3).</param>
|
||||
/// <param name="delayMs">Delay between retries in milliseconds (default: 100).</param>
|
||||
/// <param name="operationName">Optional name for logging purposes.</param>
|
||||
/// <returns>The result of the operation, or default if all retries failed.</returns>
|
||||
public static T? ExecuteWithRetry<T>(
|
||||
Func<T?> operation,
|
||||
Func<T?, bool> isValid,
|
||||
int maxRetries = DefaultMaxRetries,
|
||||
int delayMs = DefaultRetryDelayMs,
|
||||
string? operationName = null)
|
||||
{
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = operation();
|
||||
|
||||
if (isValid(result))
|
||||
{
|
||||
if (attempt > 0 && !string.IsNullOrEmpty(operationName))
|
||||
{
|
||||
Logger.LogDebug($"[Retry] {operationName} succeeded on attempt {attempt + 1}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(operationName))
|
||||
{
|
||||
Logger.LogWarning($"[Retry] {operationName} returned invalid result on attempt {attempt + 1}, retrying...");
|
||||
}
|
||||
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(operationName))
|
||||
{
|
||||
Logger.LogWarning($"[Retry] {operationName} failed on attempt {attempt + 1}: {ex.Message}, retrying...");
|
||||
}
|
||||
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(operationName))
|
||||
{
|
||||
Logger.LogWarning($"[Retry] {operationName} failed after {maxRetries} attempts: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(operationName))
|
||||
{
|
||||
Logger.LogWarning($"[Retry] {operationName} failed after {maxRetries} attempts");
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,7 +565,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
|
||||
|
||||
/// <summary>
|
||||
/// Collection of available profiles (for button display)
|
||||
/// Gets or sets collection of available profiles (for button display)
|
||||
/// </summary>
|
||||
public ObservableCollection<PowerDisplayProfile> Profiles
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user