Concurrency crashes - H1 HardwareMonitorService.RebuildTree now builds the tree under _updateLock, so it can't enumerate a hardware's _active HashSet while the update loop mutates it. - A1 AppSettings now guards every dictionary read/write (and snapshots in Save) with a lock — safe under concurrent access from the parallel discovery threads. - R2 Plumbed that same lock (HardwareMonitorService.SensorReadLock) into RemoteWebServer and wrapped the Prometheus sensor.Values enumeration with it. - L1 Computer — refactored Add into AddCore, which performs the cancellation/enabled re-check and the _groups insertion atomically under _lock. A deferred task can no longer add (and leak) a group after Close() drained the list; if it loses the race it closes the group instead. - M1 UpdateTimer_Tick now bails before/after the await when _isShuttingDown is set in MainWindow_Closed, so an in-flight tick won't touch the disposed view-model/Computer. Broken behavior - T1 Tray callback now decodes NOTIFYICON_VERSION_4 correctly (message = LOWORD(lParam), icon id = HIWORD(lParam)) — right-click menu and double-click work again. - M2 A transient update exception no longer calls _timer.Stop(); the loop keeps running. - H2 Newly discovered (deferred) storage devices get the current ForceDriveWakeup setting applied in HardwareChanged. - V2 Sensor items carry a parent reference; toggling IsVisible recomputes the parent group's visibility, so no empty group headers. (Strengthened the existing test that had skipped this assertion.) - H3 Tree-rebuild coalescing now uses a dirty flag with a re-check, so a change arriving during a rebuild isn't lost. - R4 Web routing matches endpoints exactly on the query-stripped path (Url.AbsolutePath), so static assets like metrics.html aren't hijacked. - L8 IntelCpu.Update skips the bus/core-clock math while TimeStampCounterFrequency is still 0 (deferred-TSC window), so clocks keep their prior value instead of reporting 0 MHz. - V1 Existing plot series keep their assigned color; only an explicit user pen color updates them (no per-tick color shifting). - M3 Runtime errors write to a dedicated runtime.log (once), instead of overwriting the shared startup.log. - T2 CreateSensorIcon returns IntPtr.Zero on DIB failure instead of the shared main-icon handle (which callers DestroyIcon). I also set _isOpen = false in HardwareMonitorService.Dispose so the rebuild guard actually holds during shutdown (the latent after-close-rebuild issue adjacent to H3
261 lines
7.5 KiB
C#
261 lines
7.5 KiB
C#
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
// Copyright (C) LibreHardwareMonitor and Contributors.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using LibreHardwareMonitor.Hardware;
|
|
using Windows.UI;
|
|
|
|
namespace LibreHardwareMonitor.Windows.WinUI.Services;
|
|
|
|
public sealed class AppSettings : ISettings
|
|
{
|
|
// ISettings is read and written concurrently from background hardware-discovery threads (deferred group/sensor
|
|
// construction) as well as the UI thread, so every access to the backing dictionary is guarded by _lock.
|
|
private readonly Dictionary<string, string> _settings = new();
|
|
private readonly object _lock = new();
|
|
|
|
private AppSettings(string fileName)
|
|
{
|
|
FileName = fileName;
|
|
}
|
|
|
|
public string FileName { get; }
|
|
|
|
public static AppSettings LoadDefault()
|
|
{
|
|
string fileName = Path.ChangeExtension(Environment.ProcessPath, ".config")
|
|
?? Path.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.config");
|
|
AppSettings settings = new(fileName);
|
|
settings.Load();
|
|
return settings;
|
|
}
|
|
|
|
public bool Contains(string name)
|
|
{
|
|
lock (_lock)
|
|
return _settings.ContainsKey(name);
|
|
}
|
|
|
|
public string GetValue(string name, string value)
|
|
{
|
|
lock (_lock)
|
|
return _settings.TryGetValue(name, out string? result) ? result : value;
|
|
}
|
|
|
|
public int GetValue(string name, int value)
|
|
{
|
|
lock (_lock)
|
|
return _settings.TryGetValue(name, out string? result) && int.TryParse(result, out int parsedValue)
|
|
? parsedValue
|
|
: value;
|
|
}
|
|
|
|
public float GetValue(string name, float value)
|
|
{
|
|
lock (_lock)
|
|
return _settings.TryGetValue(name, out string? result)
|
|
&& float.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue)
|
|
? parsedValue
|
|
: value;
|
|
}
|
|
|
|
public double GetValue(string name, double value)
|
|
{
|
|
lock (_lock)
|
|
return _settings.TryGetValue(name, out string? result)
|
|
&& double.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedValue)
|
|
? parsedValue
|
|
: value;
|
|
}
|
|
|
|
public bool GetValue(string name, bool value)
|
|
{
|
|
lock (_lock)
|
|
return _settings.TryGetValue(name, out string? result) ? result == "true" : value;
|
|
}
|
|
|
|
public Color GetValue(string name, Color value)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_settings.TryGetValue(name, out string? result)
|
|
&& uint.TryParse(result, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint parsedValue))
|
|
{
|
|
return Color.FromArgb(
|
|
(byte)((parsedValue >> 24) & 0xff),
|
|
(byte)((parsedValue >> 16) & 0xff),
|
|
(byte)((parsedValue >> 8) & 0xff),
|
|
(byte)(parsedValue & 0xff));
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
public void SetValue(string name, string value)
|
|
{
|
|
lock (_lock)
|
|
_settings[name] = value;
|
|
}
|
|
|
|
public void SetValue(string name, int value)
|
|
{
|
|
lock (_lock)
|
|
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public void SetValue(string name, float value)
|
|
{
|
|
lock (_lock)
|
|
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public void SetValue(string name, double value)
|
|
{
|
|
lock (_lock)
|
|
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public void SetValue(string name, bool value)
|
|
{
|
|
lock (_lock)
|
|
_settings[name] = value ? "true" : "false";
|
|
}
|
|
|
|
public void SetValue(string name, Color value)
|
|
{
|
|
uint argb = (uint)((value.A << 24) | (value.R << 16) | (value.G << 8) | value.B);
|
|
lock (_lock)
|
|
_settings[name] = argb.ToString("X8", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
public void Remove(string name)
|
|
{
|
|
lock (_lock)
|
|
_settings.Remove(name);
|
|
}
|
|
|
|
public void Load()
|
|
{
|
|
XmlDocument doc = new();
|
|
try
|
|
{
|
|
doc.Load(FileName);
|
|
}
|
|
catch
|
|
{
|
|
if (!TryLoadBackup(doc))
|
|
return;
|
|
}
|
|
|
|
XmlNodeList list = doc.GetElementsByTagName("appSettings");
|
|
foreach (XmlNode node in list)
|
|
{
|
|
if (node.ParentNode?.Name != "configuration" || node.ParentNode.ParentNode is not XmlDocument)
|
|
continue;
|
|
|
|
foreach (XmlNode child in node.ChildNodes)
|
|
{
|
|
if (child.Name != "add")
|
|
continue;
|
|
|
|
XmlAttribute? keyAttribute = child.Attributes?["key"];
|
|
XmlAttribute? valueAttribute = child.Attributes?["value"];
|
|
if (keyAttribute?.Value != null && valueAttribute?.Value != null)
|
|
{
|
|
lock (_lock)
|
|
_settings[keyAttribute.Value] = valueAttribute.Value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(FileName) ?? AppContext.BaseDirectory);
|
|
|
|
XmlDocument doc = new();
|
|
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
|
|
XmlElement configuration = doc.CreateElement("configuration");
|
|
doc.AppendChild(configuration);
|
|
XmlElement appSettings = doc.CreateElement("appSettings");
|
|
configuration.AppendChild(appSettings);
|
|
|
|
List<KeyValuePair<string, string>> snapshot;
|
|
lock (_lock)
|
|
snapshot = new List<KeyValuePair<string, string>>(_settings);
|
|
|
|
foreach (KeyValuePair<string, string> setting in snapshot)
|
|
{
|
|
XmlElement add = doc.CreateElement("add");
|
|
add.SetAttribute("key", setting.Key);
|
|
add.SetAttribute("value", setting.Value);
|
|
appSettings.AppendChild(add);
|
|
}
|
|
|
|
byte[] file;
|
|
using (MemoryStream memory = new())
|
|
{
|
|
using (StreamWriter writer = new(memory, Encoding.UTF8, leaveOpen: true))
|
|
doc.Save(writer);
|
|
file = memory.ToArray();
|
|
}
|
|
|
|
string backupFileName = FileName + ".backup";
|
|
if (File.Exists(FileName))
|
|
{
|
|
TryDelete(backupFileName);
|
|
TryMove(FileName, backupFileName);
|
|
}
|
|
|
|
using (FileStream stream = new(FileName, FileMode.Create, FileAccess.Write))
|
|
stream.Write(file, 0, file.Length);
|
|
|
|
TryDelete(backupFileName);
|
|
}
|
|
|
|
private bool TryLoadBackup(XmlDocument doc)
|
|
{
|
|
TryDelete(FileName);
|
|
string backupFileName = FileName + ".backup";
|
|
try
|
|
{
|
|
doc.Load(backupFileName);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
TryDelete(backupFileName);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void TryDelete(string fileName)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(fileName);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
private static void TryMove(string sourceFileName, string destinationFileName)
|
|
{
|
|
try
|
|
{
|
|
File.Move(sourceFileName, destinationFileName);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|