mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +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.Extensions.Logging" Version="5.3.8" />
|
||||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||||
|
<PackageVersion Include="Polly.Core" Version="8.6.5" />
|
||||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||||
|
|||||||
@@ -1587,6 +1587,7 @@ SOFTWARE.
|
|||||||
- NLog.Extensions.Logging
|
- NLog.Extensions.Logging
|
||||||
- NLog.Schema
|
- NLog.Schema
|
||||||
- OpenAI
|
- OpenAI
|
||||||
|
- Polly.Core
|
||||||
- ReverseMarkdown
|
- ReverseMarkdown
|
||||||
- ScipBe.Common.Office.OneNote
|
- ScipBe.Common.Office.OneNote
|
||||||
- SharpCompress
|
- SharpCompress
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
using PowerDisplay.Common.Interfaces;
|
using PowerDisplay.Common.Interfaces;
|
||||||
using PowerDisplay.Common.Models;
|
using PowerDisplay.Common.Models;
|
||||||
using PowerDisplay.Common.Utils;
|
using PowerDisplay.Common.Utils;
|
||||||
@@ -45,6 +47,42 @@ namespace PowerDisplay.Common.Drivers.DDC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const int RetryDelayMs = 100;
|
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 PhysicalMonitorHandleManager _handleManager = new();
|
||||||
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
||||||
|
|
||||||
@@ -142,39 +180,33 @@ namespace PowerDisplay.Common.Drivers.DDC
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Step 1: Get capabilities string length with retry
|
// 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;
|
return 0u;
|
||||||
},
|
});
|
||||||
len => len > 0,
|
|
||||||
maxRetries: 3,
|
|
||||||
delayMs: RetryDelayMs,
|
|
||||||
operationName: "GetCapabilitiesStringLength");
|
|
||||||
|
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
|
Logger.LogWarning("[Retry] GetCapabilitiesStringLength failed after 3 attempts");
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Get actual capabilities string with retry
|
// Step 2: Get actual capabilities string with retry
|
||||||
var capsString = RetryHelper.ExecuteWithRetry(
|
var capsString = CapabilitiesStringRetryPipeline.Execute(
|
||||||
() => TryGetCapabilitiesString(monitor.Handle, length),
|
() => TryGetCapabilitiesString(monitor.Handle, length));
|
||||||
str => !string.IsNullOrEmpty(str),
|
|
||||||
maxRetries: 5,
|
|
||||||
delayMs: RetryDelayMs,
|
|
||||||
operationName: "GetCapabilitiesString");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(capsString))
|
if (!string.IsNullOrEmpty(capsString))
|
||||||
{
|
{
|
||||||
Logger.LogDebug($"Got capabilities string (length: {capsString.Length})");
|
Logger.LogDebug($"Got capabilities string (length: {capsString.Length})");
|
||||||
return capsString;
|
return capsString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogWarning("[Retry] GetCapabilitiesString failed after 5 attempts");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using PowerDisplay.Common.Utils;
|
|
||||||
using static PowerDisplay.Common.Drivers.PInvoke;
|
using static PowerDisplay.Common.Drivers.PInvoke;
|
||||||
|
|
||||||
namespace PowerDisplay.Common.Drivers.DDC
|
namespace PowerDisplay.Common.Drivers.DDC
|
||||||
@@ -16,7 +17,8 @@ namespace PowerDisplay.Common.Drivers.DDC
|
|||||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||||
{
|
{
|
||||||
// Mapping: monitorId -> physical handle (thread-safe)
|
// 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;
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -24,28 +26,28 @@ namespace PowerDisplay.Common.Drivers.DDC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
|
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
|
// Clean up unused handles before updating
|
||||||
CleanupUnusedHandles(dict, newHandleMap);
|
CleanupUnusedHandles(newHandleMap);
|
||||||
|
|
||||||
// Update the device key map
|
// Update the device key map
|
||||||
dict.Clear();
|
_monitorIdToHandleMap.Clear();
|
||||||
foreach (var kvp in newHandleMap)
|
foreach (var kvp in newHandleMap)
|
||||||
{
|
{
|
||||||
dict[kvp.Key] = kvp.Value;
|
_monitorIdToHandleMap[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clean up handles that are no longer in use.
|
/// Clean up handles that are no longer in use.
|
||||||
/// Called within ExecuteWithLock context with the internal dictionary.
|
/// Called within lock context. Optimized to O(n) using HashSet lookup.
|
||||||
/// Optimized to O(n) using HashSet lookup instead of O(n*m) nested loops.
|
|
||||||
/// </summary>
|
/// </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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -54,14 +56,9 @@ namespace PowerDisplay.Common.Drivers.DDC
|
|||||||
var reusedHandles = new HashSet<IntPtr>(newHandles.Values);
|
var reusedHandles = new HashSet<IntPtr>(newHandles.Values);
|
||||||
|
|
||||||
// Find handles to destroy: in old map but not reused (O(n) with O(1) lookup)
|
// Find handles to destroy: in old map but not reused (O(n) with O(1) lookup)
|
||||||
var handlesToDestroy = new List<IntPtr>();
|
var handlesToDestroy = _monitorIdToHandleMap.Values
|
||||||
foreach (var oldHandle in currentHandles.Values)
|
.Where(h => h != IntPtr.Zero && !reusedHandles.Contains(h))
|
||||||
{
|
.ToList();
|
||||||
if (oldHandle != IntPtr.Zero && !reusedHandles.Contains(oldHandle))
|
|
||||||
{
|
|
||||||
handlesToDestroy.Add(oldHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy unused handles
|
// Destroy unused handles
|
||||||
foreach (var handle in handlesToDestroy)
|
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
|
// 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)
|
foreach (var handle in handles)
|
||||||
{
|
{
|
||||||
if (handle != IntPtr.Zero)
|
if (handle != IntPtr.Zero)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Polly.Core" />
|
||||||
<PackageReference Include="WmiLight" />
|
<PackageReference Include="WmiLight" />
|
||||||
<PackageReference Include="System.Collections.Immutable" />
|
<PackageReference Include="System.Collections.Immutable" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -23,7 +24,8 @@ namespace PowerDisplay.Common.Services
|
|||||||
public partial class MonitorStateManager : IDisposable
|
public partial class MonitorStateManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _stateFilePath;
|
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 readonly SimpleDebouncer _saveDebouncer;
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -83,39 +85,35 @@ namespace PowerDisplay.Common.Services
|
|||||||
return;
|
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
|
case "Brightness":
|
||||||
if (!dict.TryGetValue(hardwareId, out var state))
|
state.Brightness = value;
|
||||||
{
|
break;
|
||||||
state = new MonitorState();
|
case "ColorTemperature":
|
||||||
dict[hardwareId] = state;
|
state.ColorTemperatureVcp = value;
|
||||||
}
|
break;
|
||||||
|
case "Contrast":
|
||||||
// Update the specific property
|
state.Contrast = value;
|
||||||
switch (property)
|
break;
|
||||||
{
|
case "Volume":
|
||||||
case "Brightness":
|
state.Volume = value;
|
||||||
state.Brightness = value;
|
break;
|
||||||
break;
|
default:
|
||||||
case "ColorTemperature":
|
Logger.LogWarning($"Unknown property: {property}");
|
||||||
state.ColorTemperatureVcp = value;
|
shouldSave = false;
|
||||||
break;
|
break;
|
||||||
case "Contrast":
|
}
|
||||||
state.Contrast = value;
|
|
||||||
break;
|
|
||||||
case "Volume":
|
|
||||||
state.Volume = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.LogWarning($"Unknown property: {property}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (shouldSave)
|
||||||
|
{
|
||||||
// Mark dirty for flush on dispose
|
// Mark dirty for flush on dispose
|
||||||
_isDirty = true;
|
_isDirty = true;
|
||||||
return true;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Schedule debounced save (SimpleDebouncer handles cancellation of previous calls)
|
// Schedule debounced save (SimpleDebouncer handles cancellation of previous calls)
|
||||||
if (shouldSave)
|
if (shouldSave)
|
||||||
@@ -141,7 +139,7 @@ namespace PowerDisplay.Common.Services
|
|||||||
return null;
|
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);
|
return (state.Brightness, state.ColorTemperatureVcp, state.Contrast, state.Volume);
|
||||||
}
|
}
|
||||||
@@ -167,23 +165,20 @@ namespace PowerDisplay.Common.Services
|
|||||||
|
|
||||||
if (stateFile?.Monitors != null)
|
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
|
_states[monitorKey] = new MonitorState
|
||||||
{
|
{
|
||||||
Brightness = entry.Brightness,
|
Brightness = entry.Brightness,
|
||||||
ColorTemperatureVcp = entry.ColorTemperatureVcp,
|
ColorTemperatureVcp = entry.ColorTemperatureVcp,
|
||||||
Contrast = entry.Contrast,
|
Contrast = entry.Contrast,
|
||||||
Volume = entry.Volume,
|
Volume = entry.Volume,
|
||||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
Logger.LogInfo($"[State] Loaded state for {stateFile.Monitors.Count} monitors from {_stateFilePath}");
|
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)}");
|
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", stateFile.Monitors.Keys)}");
|
||||||
@@ -258,24 +253,21 @@ namespace PowerDisplay.Common.Services
|
|||||||
LastUpdated = now,
|
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
|
stateFile.Monitors[monitorId] = new MonitorStateEntry
|
||||||
{
|
{
|
||||||
Brightness = state.Brightness,
|
Brightness = state.Brightness,
|
||||||
ColorTemperatureVcp = state.ColorTemperatureVcp,
|
ColorTemperatureVcp = state.ColorTemperatureVcp,
|
||||||
Contrast = state.Contrast,
|
Contrast = state.Contrast,
|
||||||
Volume = state.Volume,
|
Volume = state.Volume,
|
||||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||||
LastUpdated = now,
|
LastUpdated = now,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
|
var json = JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
|
||||||
return (json, stateFile.Monitors.Count);
|
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>();
|
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Collection of available profiles (for button display)
|
/// Gets or sets collection of available profiles (for button display)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ObservableCollection<PowerDisplayProfile> Profiles
|
public ObservableCollection<PowerDisplayProfile> Profiles
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user