cleanup after DI graph rewrite
This commit is contained in:
@@ -18,6 +18,10 @@ public sealed class Logger : ILogger
|
||||
private readonly IComputer _computer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly string _baseDirectory;
|
||||
// Guards the _sensors/_identifiers pair: the timer thread reassigns them on rotation while background
|
||||
// hardware-discovery threads read/write them via SensorAdded/SensorRemoved (Computer.HardwareAdded now fires
|
||||
// off the caller thread once any deferred-detection flag is active).
|
||||
private readonly object _sync = new();
|
||||
private DateTime _day = DateTime.MinValue;
|
||||
private string _fileName = "";
|
||||
private string[]? _identifiers;
|
||||
@@ -76,21 +80,28 @@ public sealed class Logger : ILogger
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshot the sensor array under the lock so a concurrent SensorAdded/SensorRemoved, or a rotation that swaps
|
||||
// in a different-length array, on a discovery thread cannot tear the read below. Element reads stay safe: a
|
||||
// reference write to a slot is atomic, so each read yields either a sensor or null.
|
||||
ISensor?[]? sensors;
|
||||
lock (_sync)
|
||||
sensors = _sensors;
|
||||
|
||||
try
|
||||
{
|
||||
using StreamWriter writer = new(new FileStream(_fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite));
|
||||
writer.Write(now.ToString("G", CultureInfo.InvariantCulture));
|
||||
writer.Write(",");
|
||||
|
||||
if (_sensors != null)
|
||||
if (sensors != null)
|
||||
{
|
||||
for (int i = 0; i < _sensors.Length; i++)
|
||||
for (int i = 0; i < sensors.Length; i++)
|
||||
{
|
||||
float? value = _sensors[i]?.Value;
|
||||
float? value = sensors[i]?.Value;
|
||||
if (value.HasValue)
|
||||
writer.Write(value.Value.ToString("R", CultureInfo.InvariantCulture));
|
||||
|
||||
writer.Write(i < _sensors.Length - 1 ? "," : Environment.NewLine);
|
||||
writer.Write(i < sensors.Length - 1 ? "," : Environment.NewLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,12 +123,21 @@ public sealed class Logger : ILogger
|
||||
SensorVisitor visitor = new(sensor => sensors.Add(sensor));
|
||||
visitor.VisitComputer(_computer);
|
||||
|
||||
_sensors = sensors.Cast<ISensor?>().ToArray();
|
||||
_identifiers = sensors.Select(sensor => sensor.Identifier.ToString()).ToArray();
|
||||
ISensor?[] sensorArray = sensors.Cast<ISensor?>().ToArray();
|
||||
string[] identifiers = sensors.Select(sensor => sensor.Identifier.ToString()).ToArray();
|
||||
|
||||
// Publish the paired arrays together under the lock so a concurrent SensorAdded/SensorRemoved never observes
|
||||
// mismatched lengths. VisitComputer and the file write run outside the lock to avoid coupling with Computer's
|
||||
// own locks.
|
||||
lock (_sync)
|
||||
{
|
||||
_sensors = sensorArray;
|
||||
_identifiers = identifiers;
|
||||
}
|
||||
|
||||
using StreamWriter writer = new(_fileName, false);
|
||||
writer.Write(",");
|
||||
writer.WriteLine(string.Join(",", _identifiers));
|
||||
writer.WriteLine(string.Join(",", identifiers));
|
||||
writer.Write("Time,");
|
||||
writer.WriteLine(string.Join(",", sensors.Select(sensor => $"\"{sensor.Name}\"")));
|
||||
}
|
||||
@@ -127,6 +147,7 @@ public sealed class Logger : ILogger
|
||||
if (!File.Exists(_fileName))
|
||||
return false;
|
||||
|
||||
string[] identifiers;
|
||||
try
|
||||
{
|
||||
using StreamReader reader = new(_fileName);
|
||||
@@ -134,30 +155,35 @@ public sealed class Logger : ILogger
|
||||
if (string.IsNullOrEmpty(line))
|
||||
return false;
|
||||
|
||||
_identifiers = line.Split(',').Skip(1).ToArray();
|
||||
identifiers = line.Split(',').Skip(1).ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_identifiers = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_identifiers.Length == 0)
|
||||
{
|
||||
_identifiers = null;
|
||||
if (identifiers.Length == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
_sensors = new ISensor?[_identifiers.Length];
|
||||
// Build into local arrays, then publish the pair together under the lock, so a discovery thread reading
|
||||
// _sensors/_identifiers never observes a half-populated or length-mismatched pair.
|
||||
ISensor?[] sensors = new ISensor?[identifiers.Length];
|
||||
SensorVisitor visitor = new(sensor =>
|
||||
{
|
||||
for (int i = 0; i < _identifiers.Length; i++)
|
||||
for (int i = 0; i < identifiers.Length; i++)
|
||||
{
|
||||
if (sensor.Identifier.ToString() == _identifiers[i])
|
||||
_sensors[i] = sensor;
|
||||
if (sensor.Identifier.ToString() == identifiers[i])
|
||||
sensors[i] = sensor;
|
||||
}
|
||||
});
|
||||
visitor.VisitComputer(_computer);
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_identifiers = identifiers;
|
||||
_sensors = sensors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,25 +213,32 @@ public sealed class Logger : ILogger
|
||||
|
||||
private void SensorAdded(ISensor sensor)
|
||||
{
|
||||
if (_sensors == null || _identifiers == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _sensors.Length; i++)
|
||||
lock (_sync)
|
||||
{
|
||||
if (sensor.Identifier.ToString() == _identifiers[i])
|
||||
_sensors[i] = sensor;
|
||||
if (_sensors == null || _identifiers == null)
|
||||
return;
|
||||
|
||||
// _sensors and _identifiers are always published together with equal lengths, so indexing both is safe.
|
||||
for (int i = 0; i < _sensors.Length; i++)
|
||||
{
|
||||
if (sensor.Identifier.ToString() == _identifiers[i])
|
||||
_sensors[i] = sensor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SensorRemoved(ISensor sensor)
|
||||
{
|
||||
if (_sensors == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _sensors.Length; i++)
|
||||
lock (_sync)
|
||||
{
|
||||
if (sensor == _sensors[i])
|
||||
_sensors[i] = null;
|
||||
if (_sensors == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _sensors.Length; i++)
|
||||
{
|
||||
if (sensor == _sensors[i])
|
||||
_sensors[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ internal static class PasswordHasher
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject a missing salt or hash segment: an empty expected hash would make FixedTimeEquals(empty, empty) return
|
||||
// true and authenticate any password. Hash() never produces this, so it only arises from a corrupted value.
|
||||
if (salt.Length == 0 || expected.Length == 0)
|
||||
return false;
|
||||
|
||||
byte[] actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length);
|
||||
return CryptographicOperations.FixedTimeEquals(actual, expected);
|
||||
}
|
||||
|
||||
@@ -408,6 +408,13 @@ public sealed class RemoteWebServer : IRemoteWebServer
|
||||
sensor.Control.SetSoftware(float.Parse(value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// Reused across requests: building a JsonSerializerOptions per request defeats System.Text.Json's per-options
|
||||
// metadata cache, which matters because data.json is polled frequently.
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
|
||||
private async Task SendJsonAsync(HttpListenerResponse response, HttpListenerRequest request)
|
||||
{
|
||||
Dictionary<string, object?> json = new()
|
||||
@@ -425,12 +432,7 @@ public sealed class RemoteWebServer : IRemoteWebServer
|
||||
SensorTreeItemViewModel? root = _rootProvider();
|
||||
json["Children"] = root == null ? Array.Empty<object>() : new List<object> { GenerateJsonForNode(root, ref nodeIndex) };
|
||||
|
||||
JsonSerializerOptions options = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(json, options));
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(json, JsonOptions));
|
||||
bool acceptGzip = request.Headers["Accept-Encoding"]?.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
WriteCommonHeaders(response);
|
||||
|
||||
@@ -94,6 +94,16 @@ internal sealed class SensorIconRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a key identifying the pixels <see cref="CreateIcon" /> would produce (the drawn text and the background
|
||||
/// color). When it is unchanged the previously created icon can be reused instead of re-rendering a new GDI icon.
|
||||
/// </summary>
|
||||
public string GetRenderKey(ISensor sensor)
|
||||
{
|
||||
WinUIColor color = GetSensorTrayColor(sensor);
|
||||
return $"{GetSensorIconText(sensor)}|{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
}
|
||||
|
||||
private WinUIColor GetSensorTrayColor(ISensor sensor)
|
||||
{
|
||||
WinUIColor defaultColor = sensor.SensorType is SensorType.Load or SensorType.Control or SensorType.Level
|
||||
|
||||
@@ -158,8 +158,17 @@ public sealed class TrayIconService : IDisposable
|
||||
|
||||
private void Update(SensorTrayIcon icon)
|
||||
{
|
||||
string renderKey = _iconRenderer.GetRenderKey(icon.Sensor);
|
||||
if (icon.IconHandle != IntPtr.Zero && renderKey == icon.LastRenderKey)
|
||||
{
|
||||
// The icon pixels are unchanged; refresh only the (cheap) tooltip and skip the GDI re-render + handle churn.
|
||||
ModifyNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(icon.Sensor));
|
||||
return;
|
||||
}
|
||||
|
||||
IntPtr previousIcon = icon.IconHandle;
|
||||
icon.IconHandle = _iconRenderer.CreateIcon(icon.Sensor);
|
||||
icon.LastRenderKey = renderKey;
|
||||
ModifyNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(icon.Sensor));
|
||||
if (previousIcon != IntPtr.Zero)
|
||||
DestroyIcon(previousIcon);
|
||||
@@ -373,5 +382,8 @@ public sealed class TrayIconService : IDisposable
|
||||
public IntPtr IconHandle { get; set; }
|
||||
|
||||
public ISensor Sensor { get; set; } = sensor;
|
||||
|
||||
/// <summary>Key of the pixels currently rendered into <see cref="IconHandle" />, used to skip redundant re-renders.</summary>
|
||||
public string? LastRenderKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
private bool _isStarted;
|
||||
private bool _isStarting;
|
||||
private int _rootUpdateQueued;
|
||||
private int _statusHardwareCount;
|
||||
private int _statusSensorCount;
|
||||
private string _statusText = "";
|
||||
private TemperatureUnit _temperatureUnit;
|
||||
private int _updateIntervalIndex;
|
||||
@@ -1041,14 +1043,21 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
MeasureStartup("MainWindowViewModel.ConfigureRoot", () => root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn), GetRootDetail);
|
||||
MeasureStartup("MainWindowViewModel.RootItems.Add", () => RootItems.Add(root), GetRootDetail);
|
||||
MeasureStartup("MainWindowViewModel.ApplySensorValuesTimeWindow", ApplySensorValuesTimeWindow, GetRootDetail);
|
||||
RefreshStatusCounts();
|
||||
}
|
||||
|
||||
// Hardware/sensor counts change only when the tree is rebuilt, so cache them here (called from UpdateRoot) instead
|
||||
// of re-walking the whole tree and re-aggregating Computer.Hardware on every per-tick UpdateStatus().
|
||||
private void RefreshStatusCounts()
|
||||
{
|
||||
_statusHardwareCount = _hardwareMonitor.Computer.Hardware.Count;
|
||||
_statusSensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
int hardwareCount = _hardwareMonitor.Computer.Hardware.Count;
|
||||
int sensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0;
|
||||
string webServerStatus = RunWebServer ? $", web server {WebServerUrl}" : "";
|
||||
StatusText = $"{hardwareCount} hardware devices, {sensorCount} sensors, update every {FormatInterval(UpdateInterval)}{webServerStatus}";
|
||||
StatusText = $"{_statusHardwareCount} hardware devices, {_statusSensorCount} sensors, update every {FormatInterval(UpdateInterval)}{webServerStatus}";
|
||||
}
|
||||
|
||||
private void RestartWebServerIfRunning()
|
||||
|
||||
@@ -78,17 +78,20 @@ internal sealed class PlotTrackingService
|
||||
series.Color = sensorItem.PenColor.Value;
|
||||
}
|
||||
|
||||
// The pipeline below sorts and de-duplicates, so there is no need to OrderBy the whole (potentially 24h)
|
||||
// history every tick; track the latest timestamp in the same pass instead of assuming the list is ordered.
|
||||
List<PlotPointViewModel> points = [];
|
||||
foreach (SensorValue sensorValue in sensor.Values.OrderBy(value => value.Time))
|
||||
DateTime? latestHistoryTimestamp = null;
|
||||
foreach (SensorValue sensorValue in sensor.Values)
|
||||
{
|
||||
double? displayedValue = SensorFormatter.GetPlotValue(sensor, sensorValue.Value, temperatureUnit);
|
||||
if (displayedValue is not { } pointValue || !double.IsFinite(pointValue))
|
||||
continue;
|
||||
|
||||
points.Add(new PlotPointViewModel(sensorValue.Time, pointValue));
|
||||
if (!latestHistoryTimestamp.HasValue || sensorValue.Time > latestHistoryTimestamp.Value)
|
||||
latestHistoryTimestamp = sensorValue.Time;
|
||||
}
|
||||
|
||||
DateTime? latestHistoryTimestamp = points.Count > 0 ? points[^1].Timestamp : null;
|
||||
foreach (PlotPointViewModel existingPoint in series.Points)
|
||||
{
|
||||
if (now - existingPoint.Timestamp > MaximumSyntheticPlotPointRetention)
|
||||
|
||||
Reference in New Issue
Block a user