3d1c55468b
- Add detailed CPU core monitoring option for better performance control - Update monitoring settings in app configuration - Improve parallel task execution for resource usage monitoring - Modify Telegram notification service to skip alerts from svchost processes - Add "Memory %" column to process table in HTML and update related JavaScript - Create performance test scripts for API response time evaluation
1006 lines
47 KiB
C#
1006 lines
47 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ResourceMonitorService.Configuration;
|
|
using ResourceMonitorService.Models;
|
|
using System.Diagnostics;
|
|
using System.Management;
|
|
|
|
namespace ResourceMonitorService.Services
|
|
{
|
|
public interface IResourceMonitorService
|
|
{
|
|
Task<ResourceUsage> GetResourceUsageAsync();
|
|
Task<CpuUsage> GetCpuUsageAsync();
|
|
Task<MemoryUsage> GetMemoryUsageAsync();
|
|
Task<GpuUsage> GetGpuUsageAsync();
|
|
Task<List<DiskUsage>> GetDiskUsageAsync();
|
|
Task<List<ProcessInfo>> GetTopProcessesAsync(int count = 10);
|
|
}
|
|
|
|
public class ResourceMonitorService : IResourceMonitorService
|
|
{
|
|
private readonly ILogger<ResourceMonitorService> _logger;
|
|
private readonly MonitoringSettings _settings;
|
|
private readonly Dictionary<string, PerformanceCounter> _counters;
|
|
private readonly Dictionary<string, long> _previousDiskBytes;
|
|
private readonly Dictionary<string, DateTime> _previousDiskTime;
|
|
private readonly Dictionary<string, int> _errorCounts;
|
|
private readonly Dictionary<int, (TimeSpan ProcessorTime, DateTime Timestamp)> _previousProcessorTimes;
|
|
|
|
public ResourceMonitorService(ILogger<ResourceMonitorService> logger, IOptions<MonitoringSettings> settings)
|
|
{
|
|
_logger = logger;
|
|
_settings = settings.Value;
|
|
_counters = new Dictionary<string, PerformanceCounter>();
|
|
_previousDiskBytes = new Dictionary<string, long>();
|
|
_previousDiskTime = new Dictionary<string, DateTime>();
|
|
_errorCounts = new Dictionary<string, int>();
|
|
_previousProcessorTimes = new Dictionary<int, (TimeSpan ProcessorTime, DateTime Timestamp)>();
|
|
InitializeCounters();
|
|
}
|
|
|
|
private void InitializeCounters()
|
|
{
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
_counters["cpu"] = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
|
_counters["memory_available"] = new PerformanceCounter("Memory", "Available MBytes");
|
|
|
|
if (_settings.EnableDiskMonitoring)
|
|
{
|
|
_counters["disk_read"] = new PerformanceCounter("PhysicalDisk", "Disk Read Bytes/sec", "_Total");
|
|
_counters["disk_write"] = new PerformanceCounter("PhysicalDisk", "Disk Write Bytes/sec", "_Total");
|
|
_counters["disk_time"] = new PerformanceCounter("PhysicalDisk", "% Disk Time", "_Total");
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
|
|
// Initialize counters with first reading
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
foreach (var counter in _counters.Values)
|
|
{
|
|
try
|
|
{
|
|
counter.NextValue();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("counter_init", ex, $"Failed to initialize performance counter: {counter.CounterName}");
|
|
}
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to initialize performance counters");
|
|
}
|
|
}
|
|
|
|
private void LogSuppressedWarning(string errorKey, Exception ex, string message)
|
|
{
|
|
if (!_errorCounts.ContainsKey(errorKey))
|
|
{
|
|
_errorCounts[errorKey] = 0;
|
|
}
|
|
|
|
_errorCounts[errorKey]++;
|
|
|
|
// Only log every 10th occurrence and the first 3 occurrences
|
|
if (_errorCounts[errorKey] <= 3 || _errorCounts[errorKey] % 10 == 0)
|
|
{
|
|
_logger.LogDebug(ex, "{Message} (occurrence #{Count})", message, _errorCounts[errorKey]);
|
|
}
|
|
}
|
|
|
|
public async Task<ResourceUsage> GetResourceUsageAsync()
|
|
{
|
|
var timestamp = DateTime.Now;
|
|
|
|
// Execute all monitoring tasks in parallel and capture results
|
|
var cpuTask = GetCpuUsageAsync();
|
|
var memoryTask = GetMemoryUsageAsync();
|
|
var gpuTask = _settings.EnableGpuMonitoring ? GetGpuUsageAsync() : Task.FromResult(new GpuUsage());
|
|
var diskTask = _settings.EnableDiskMonitoring ? GetDiskUsageAsync() : Task.FromResult(new List<DiskUsage>());
|
|
var processTask = _settings.EnableProcessMonitoring ? GetTopProcessesAsync(_settings.MaxProcessesToTrack) : Task.FromResult(new List<ProcessInfo>());
|
|
var temperatureTask = _settings.EnableTemperatureMonitoring ? GetTemperatureInfoAsync() : Task.FromResult(new TemperatureInfo());
|
|
|
|
// Wait for all tasks to complete
|
|
await Task.WhenAll(cpuTask, memoryTask, gpuTask, diskTask, processTask, temperatureTask);
|
|
|
|
return new ResourceUsage
|
|
{
|
|
Timestamp = timestamp,
|
|
CPU = await cpuTask,
|
|
Memory = await memoryTask,
|
|
GPU = await gpuTask,
|
|
Disks = await diskTask,
|
|
TopProcesses = await processTask,
|
|
Temperature = await temperatureTask
|
|
};
|
|
}
|
|
|
|
public async Task<CpuUsage> GetCpuUsageAsync()
|
|
{
|
|
try
|
|
{
|
|
var temperature = await GetCpuTemperatureAsync();
|
|
|
|
return await Task.Run(() =>
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
var usage = _counters.TryGetValue("cpu", out var cpuCounter) ? cpuCounter.NextValue() : 0f;
|
|
|
|
// Get per-core usage (only if enabled for performance)
|
|
var coreUsages = new List<float>();
|
|
if (_settings.EnableDetailedCpuCoreMonitoring)
|
|
{
|
|
for (int i = 0; i < Environment.ProcessorCount; i++)
|
|
{
|
|
try
|
|
{
|
|
using var coreCounter = new PerformanceCounter("Processor", "% Processor Time", i.ToString());
|
|
coreCounter.NextValue();
|
|
Thread.Sleep(50); // Reduced delay for faster reading while maintaining accuracy
|
|
coreUsages.Add(coreCounter.NextValue());
|
|
}
|
|
catch
|
|
{
|
|
coreUsages.Add(0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get CPU frequency
|
|
var maxFrequency = 0f;
|
|
var currentFrequency = 0f;
|
|
try
|
|
{
|
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
maxFrequency = Convert.ToSingle(obj["MaxClockSpeed"]);
|
|
currentFrequency = Convert.ToSingle(obj["CurrentClockSpeed"]);
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not get CPU frequency information");
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
|
|
return new CpuUsage
|
|
{
|
|
Usage = usage,
|
|
CoreUsages = coreUsages.ToArray(),
|
|
MaxFrequency = maxFrequency,
|
|
CurrentFrequency = currentFrequency,
|
|
IsThrottling = currentFrequency < maxFrequency * 0.9f, // Consider throttling if running below 90% max frequency
|
|
Temperature = temperature
|
|
};
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting CPU usage");
|
|
return new CpuUsage();
|
|
}
|
|
}
|
|
|
|
public async Task<MemoryUsage> GetMemoryUsageAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
var availableMemoryMB = _counters.TryGetValue("memory_available", out var memCounter) ? memCounter.NextValue() : 0f;
|
|
var availableMemory = (ulong)(availableMemoryMB * 1024 * 1024);
|
|
|
|
// Get total memory
|
|
var totalMemory = 0UL;
|
|
using (var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem"))
|
|
using (var collection = searcher.Get())
|
|
{
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
totalMemory = (ulong)obj["TotalPhysicalMemory"];
|
|
break;
|
|
}
|
|
}
|
|
|
|
var usedMemory = totalMemory - availableMemory;
|
|
var usagePercentage = totalMemory > 0 ? (float)(usedMemory) / totalMemory * 100 : 0f;
|
|
|
|
// Get additional memory info
|
|
var committedMemory = 0UL;
|
|
var pagedMemory = 0UL;
|
|
var nonPagedMemory = 0UL;
|
|
|
|
try
|
|
{
|
|
using var osSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem");
|
|
using var osCollection = osSearcher.Get();
|
|
foreach (ManagementObject obj in osCollection)
|
|
{
|
|
var totalVirtualMemory = (ulong)obj["TotalVirtualMemorySize"] * 1024;
|
|
var freeVirtualMemory = (ulong)obj["FreeVirtualMemory"] * 1024;
|
|
committedMemory = totalVirtualMemory - freeVirtualMemory;
|
|
break;
|
|
}
|
|
|
|
using var perfSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_PerfRawData_PerfOS_Memory");
|
|
using var perfCollection = perfSearcher.Get();
|
|
foreach (ManagementObject obj in perfCollection)
|
|
{
|
|
pagedMemory = (ulong)obj["PoolPagedBytes"];
|
|
nonPagedMemory = (ulong)obj["PoolNonpagedBytes"];
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not get extended memory information");
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
|
|
return new MemoryUsage
|
|
{
|
|
UsagePercentage = usagePercentage,
|
|
UsedMemory = usedMemory,
|
|
AvailableMemory = availableMemory,
|
|
TotalMemory = totalMemory,
|
|
CommittedMemory = committedMemory,
|
|
PagedMemory = pagedMemory,
|
|
NonPagedMemory = nonPagedMemory
|
|
};
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting memory usage");
|
|
return new MemoryUsage();
|
|
}
|
|
}
|
|
|
|
public async Task<GpuUsage> GetGpuUsageAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
NvmlWrapper.NvmlInit();
|
|
IntPtr device;
|
|
NvmlWrapper.NvmlDeviceGetHandleByIndex(0, out device);
|
|
|
|
NvmlWrapper.NvmlUtilization utilization;
|
|
NvmlWrapper.NvmlDeviceGetUtilizationRates(device, out utilization);
|
|
|
|
uint temperature;
|
|
NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature);
|
|
|
|
uint fanSpeed = 0;
|
|
var fanResult = NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed);
|
|
if (fanResult != 0)
|
|
{
|
|
fanSpeed = 0; // Reset to 0 if call failed
|
|
LogSuppressedWarning("gpu_fan", new Exception($"NVML fan speed call failed with code: {fanResult}"), "Could not get GPU fan speed");
|
|
}
|
|
|
|
uint powerUsage = 0;
|
|
var powerResult = NvmlWrapper.NvmlDeviceGetPowerUsage(device, out powerUsage);
|
|
if (powerResult != 0) powerUsage = 0; // Reset to 0 if call failed, power is in milliwatts
|
|
|
|
// Get memory information
|
|
NvmlWrapper.NvmlMemory memory;
|
|
var memoryResult = NvmlWrapper.NvmlDeviceGetMemoryInfo(device, out memory);
|
|
var memoryTotal = memoryResult == 0 ? memory.Total : 0UL;
|
|
var memoryUsed = memoryResult == 0 ? memory.Used : 0UL;
|
|
|
|
// Get GPU name via NVML
|
|
var name = "NVIDIA GPU";
|
|
var nameBuffer = new byte[256];
|
|
var nameResult = NvmlWrapper.NvmlDeviceGetName(device, nameBuffer, 256);
|
|
if (nameResult == 0)
|
|
{
|
|
name = System.Text.Encoding.ASCII.GetString(nameBuffer).TrimEnd('\0');
|
|
}
|
|
|
|
var driverVersion = "Unknown";
|
|
|
|
try
|
|
{
|
|
// Get driver version via WMI as fallback
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController WHERE Name LIKE '%NVIDIA%'");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
driverVersion = obj["DriverVersion"]?.ToString() ?? driverVersion;
|
|
// If NVML memory call failed, try WMI as fallback
|
|
if (memoryTotal == 0 && obj["AdapterRAM"] != null)
|
|
{
|
|
memoryTotal = (ulong)obj["AdapterRAM"];
|
|
}
|
|
break;
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("gpu_wmi", ex, "Could not get additional GPU information via WMI");
|
|
}
|
|
|
|
NvmlWrapper.NvmlShutdown();
|
|
|
|
return new GpuUsage
|
|
{
|
|
Usage = utilization.Gpu,
|
|
MemoryUsage = utilization.Memory,
|
|
Temperature = temperature,
|
|
FanSpeed = fanSpeed,
|
|
PowerUsage = powerUsage, // Power in milliwatts
|
|
MemoryTotal = memoryTotal,
|
|
MemoryUsed = memoryUsed,
|
|
IsAvailable = true,
|
|
Name = name,
|
|
DriverVersion = driverVersion,
|
|
Error = string.Empty
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new GpuUsage
|
|
{
|
|
Usage = 0,
|
|
MemoryUsage = 0,
|
|
Temperature = 0,
|
|
FanSpeed = 0,
|
|
PowerUsage = 0,
|
|
MemoryTotal = 0,
|
|
MemoryUsed = 0,
|
|
IsAvailable = false,
|
|
Name = "Unknown",
|
|
DriverVersion = "Unknown",
|
|
Error = ex.Message
|
|
};
|
|
}
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting GPU usage");
|
|
return new GpuUsage { Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
public async Task<List<DiskUsage>> GetDiskUsageAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var diskUsages = new List<DiskUsage>();
|
|
var timestamp = DateTime.Now;
|
|
|
|
var drives = DriveInfo.GetDrives();
|
|
foreach (var drive in drives)
|
|
{
|
|
if (drive.IsReady)
|
|
{
|
|
var diskUsage = new DiskUsage
|
|
{
|
|
DriveLetter = drive.Name,
|
|
Label = drive.VolumeLabel,
|
|
FileSystem = drive.DriveFormat,
|
|
TotalSize = (ulong)drive.TotalSize,
|
|
FreeSpace = (ulong)drive.AvailableFreeSpace,
|
|
UsedSpace = (ulong)(drive.TotalSize - drive.AvailableFreeSpace),
|
|
UsagePercentage = (float)(drive.TotalSize - drive.AvailableFreeSpace) / drive.TotalSize * 100
|
|
};
|
|
|
|
// Get disk performance data with proper timing
|
|
GetDiskPerformanceData(drive, diskUsage, timestamp);
|
|
|
|
// Get disk temperature using SMART data
|
|
GetDiskTemperature(drive, diskUsage);
|
|
|
|
// Get additional disk information
|
|
GetDiskInfo(drive, diskUsage);
|
|
|
|
diskUsages.Add(diskUsage);
|
|
}
|
|
}
|
|
|
|
return diskUsages;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting disk usage");
|
|
return new List<DiskUsage>();
|
|
}
|
|
}
|
|
|
|
private void GetDiskPerformanceData(DriveInfo drive, DiskUsage diskUsage, DateTime timestamp)
|
|
{
|
|
try
|
|
{
|
|
var diskName = drive.Name.Replace("\\", "").Replace(":", "");
|
|
var diskKey = $"disk_{diskName}";
|
|
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
// Try different counter instance names that Windows might use
|
|
var possibleNames = new[] { diskName, $"{diskName}:", drive.Name.TrimEnd('\\'), "_Total" };
|
|
|
|
foreach (var name in possibleNames)
|
|
{
|
|
try
|
|
{
|
|
using var readCounter = new PerformanceCounter("LogicalDisk", "Disk Read Bytes/sec", name);
|
|
using var writeCounter = new PerformanceCounter("LogicalDisk", "Disk Write Bytes/sec", name);
|
|
using var timeCounter = new PerformanceCounter("LogicalDisk", "% Disk Time", name);
|
|
using var readOpsCounter = new PerformanceCounter("LogicalDisk", "Disk Reads/sec", name);
|
|
using var writeOpsCounter = new PerformanceCounter("LogicalDisk", "Disk Writes/sec", name);
|
|
|
|
var readBytes = (long)readCounter.NextValue();
|
|
var writeBytes = (long)writeCounter.NextValue();
|
|
var readOps = (long)readOpsCounter.NextValue();
|
|
var writeOps = (long)writeOpsCounter.NextValue();
|
|
|
|
// Calculate speeds if we have previous data
|
|
var readKey = $"{diskKey}_read";
|
|
var writeKey = $"{diskKey}_write";
|
|
var readOpsKey = $"{diskKey}_read_ops";
|
|
var writeOpsKey = $"{diskKey}_write_ops";
|
|
|
|
if (_previousDiskBytes.ContainsKey(readKey) && _previousDiskTime.ContainsKey(readKey))
|
|
{
|
|
var timeDiff = (timestamp - _previousDiskTime[readKey]).TotalSeconds;
|
|
if (timeDiff > 0)
|
|
{
|
|
var readBytesDiff = readBytes - _previousDiskBytes[readKey];
|
|
var writeBytesDiff = writeBytes - _previousDiskBytes[writeKey];
|
|
var readOpsDiff = readOps - _previousDiskBytes[readOpsKey];
|
|
var writeOpsDiff = writeOps - _previousDiskBytes[writeOpsKey];
|
|
|
|
diskUsage.ReadSpeed = (float)(readBytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
|
diskUsage.WriteSpeed = (float)(writeBytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
|
diskUsage.ReadOperations = (long)(readOpsDiff / timeDiff);
|
|
diskUsage.WriteOperations = (long)(writeOpsDiff / timeDiff);
|
|
}
|
|
}
|
|
|
|
// Store current values for next calculation
|
|
_previousDiskBytes[readKey] = readBytes;
|
|
_previousDiskBytes[writeKey] = writeBytes;
|
|
_previousDiskBytes[readOpsKey] = readOps;
|
|
_previousDiskBytes[writeOpsKey] = writeOps;
|
|
_previousDiskTime[readKey] = timestamp;
|
|
_previousDiskTime[writeKey] = timestamp;
|
|
|
|
// Get current disk time percentage
|
|
diskUsage.DiskTime = timeCounter.NextValue();
|
|
break; // Successfully got data, exit the loop
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"disk_perf_{name}", ex, $"Could not get disk performance data for instance {name}");
|
|
}
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"disk_perf_{drive.Name}", ex, $"Could not get disk performance data for drive {drive.Name}");
|
|
}
|
|
}
|
|
|
|
private void GetDiskTemperature(DriveInfo drive, DiskUsage diskUsage)
|
|
{
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
// Try to get SMART data for temperature
|
|
var physicalDriveQuery = $@"\\.\{drive.Name.Replace("\\", "").Replace(":", "")}";
|
|
|
|
// Method 1: Try WMI Win32_DiskDrive
|
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject disk in collection)
|
|
{
|
|
var model = disk["Model"]?.ToString() ?? "";
|
|
var serialNumber = disk["SerialNumber"]?.ToString() ?? "";
|
|
|
|
// Try to get SMART data using MSStorageDriver_ATAPISmartData
|
|
try
|
|
{
|
|
using var smartSearcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
|
|
using var smartCollection = smartSearcher.Get();
|
|
foreach (ManagementObject smartData in smartCollection)
|
|
{
|
|
var vendorSpecific = smartData["VendorSpecific"] as byte[];
|
|
if (vendorSpecific != null && vendorSpecific.Length >= 362)
|
|
{
|
|
// SMART attribute 194 (0xC2) is typically temperature
|
|
// This is a simplified extraction - real implementation would need proper SMART parsing
|
|
for (int i = 2; i < 362; i += 12)
|
|
{
|
|
if (i + 11 < vendorSpecific.Length && vendorSpecific[i] == 194) // Temperature attribute
|
|
{
|
|
diskUsage.Temperature = vendorSpecific[i + 5]; // Raw value is typically temperature
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"smart_temp_{drive.Name}", ex, $"Could not get SMART temperature for {drive.Name}");
|
|
}
|
|
}
|
|
|
|
// Method 2: Try thermal zone if SMART fails
|
|
if (diskUsage.Temperature == 0)
|
|
{
|
|
using var thermalSearcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_FailurePredictStatus");
|
|
using var thermalCollection = thermalSearcher.Get();
|
|
foreach (ManagementObject obj in thermalCollection)
|
|
{
|
|
// This is a fallback method - may not provide temperature but can indicate drive health
|
|
var active = obj["Active"]?.ToString() ?? "";
|
|
var reason = obj["Reason"]?.ToString() ?? "";
|
|
if (active == "True")
|
|
{
|
|
// Drive is reporting potential failure - this doesn't give us temperature but is useful
|
|
LogSuppressedWarning($"disk_health_{drive.Name}", new Exception($"Drive health warning: {reason}"), $"Drive {drive.Name} health warning");
|
|
}
|
|
}
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"disk_temp_{drive.Name}", ex, $"Could not get disk temperature for drive {drive.Name}");
|
|
}
|
|
}
|
|
|
|
private void GetDiskInfo(DriveInfo drive, DiskUsage diskUsage)
|
|
{
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
// Get more detailed disk information
|
|
using var searcher = new ManagementObjectSearcher($"SELECT * FROM Win32_LogicalDisk WHERE DeviceID='{drive.Name.TrimEnd('\\')}'");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
// Try to determine if it's an SSD by checking the physical disk
|
|
var deviceId = obj["DeviceID"]?.ToString();
|
|
if (!string.IsNullOrEmpty(deviceId))
|
|
{
|
|
// Get the associated physical disk
|
|
using var partitionSearcher = new ManagementObjectSearcher($"ASSOCIATORS OF {{Win32_LogicalDisk.DeviceID='{deviceId}'}} WHERE AssocClass=Win32_LogicalDiskToPartition");
|
|
using var partitionCollection = partitionSearcher.Get();
|
|
foreach (ManagementObject partition in partitionCollection)
|
|
{
|
|
using var diskSearcher = new ManagementObjectSearcher($"ASSOCIATORS OF {{Win32_DiskPartition.DeviceID='{partition["DeviceID"]}'}} WHERE AssocClass=Win32_DiskDriveToDiskPartition");
|
|
using var diskCollection = diskSearcher.Get();
|
|
foreach (ManagementObject physicalDisk in diskCollection)
|
|
{
|
|
var mediaType = physicalDisk["MediaType"]?.ToString() ?? "";
|
|
var model = physicalDisk["Model"]?.ToString() ?? "";
|
|
|
|
// More sophisticated SSD detection
|
|
diskUsage.IsSSD = mediaType.Contains("SSD") ||
|
|
model.ToLower().Contains("ssd") ||
|
|
model.ToLower().Contains("solid") ||
|
|
model.ToLower().Contains("nvme") ||
|
|
CheckIfSSDByRotationRate(physicalDisk);
|
|
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"disk_info_{drive.Name}", ex, $"Could not get detailed disk information for drive {drive.Name}");
|
|
}
|
|
}
|
|
|
|
private bool CheckIfSSDByRotationRate(ManagementObject physicalDisk)
|
|
{
|
|
try
|
|
{
|
|
// Try to get rotation rate - SSDs typically report 0 or 1
|
|
var nominalMediaRotationRate = physicalDisk["NominalMediaRotationRate"];
|
|
if (nominalMediaRotationRate != null)
|
|
{
|
|
var rotationRate = Convert.ToUInt32(nominalMediaRotationRate);
|
|
return rotationRate == 0 || rotationRate == 1; // SSD indicators
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors in rotation rate detection
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public async Task<List<ProcessInfo>> GetTopProcessesAsync(int count = 10)
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var allProcesses = Process.GetProcesses();
|
|
_logger.LogDebug("Found {ProcessCount} total processes", allProcesses.Length);
|
|
|
|
var validProcesses = new List<ProcessInfo>();
|
|
int skippedCount = 0;
|
|
var currentTime = DateTime.Now;
|
|
|
|
// Get total system memory for percentage calculations
|
|
var totalSystemMemory = GetTotalSystemMemory();
|
|
|
|
foreach (var p in allProcesses)
|
|
{
|
|
try
|
|
{
|
|
if (p.HasExited)
|
|
{
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
var memoryUsage = (ulong)p.WorkingSet64;
|
|
var cpuUsage = CalculateProcessCpuUsage(p.Id, p.TotalProcessorTime, currentTime);
|
|
|
|
var processInfo = new ProcessInfo
|
|
{
|
|
ProcessId = p.Id,
|
|
Name = p.ProcessName,
|
|
MemoryUsage = memoryUsage,
|
|
MemoryUsagePercentage = totalSystemMemory > 0 ? (float)(memoryUsage * 100.0 / totalSystemMemory) : 0f,
|
|
ProcessorTime = p.TotalProcessorTime,
|
|
CpuUsage = cpuUsage
|
|
};
|
|
|
|
// Try to get additional info, but don't fail if we can't
|
|
try
|
|
{
|
|
processInfo.StartTime = p.StartTime;
|
|
}
|
|
catch
|
|
{
|
|
processInfo.StartTime = DateTime.MinValue;
|
|
}
|
|
|
|
try
|
|
{
|
|
processInfo.ExecutablePath = p.MainModule?.FileName ?? "";
|
|
}
|
|
catch
|
|
{
|
|
processInfo.ExecutablePath = "";
|
|
}
|
|
|
|
try
|
|
{
|
|
processInfo.CommandLine = GetProcessCommandLine(p.Id);
|
|
}
|
|
catch
|
|
{
|
|
processInfo.CommandLine = "";
|
|
}
|
|
|
|
validProcesses.Add(processInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
skippedCount++;
|
|
_logger.LogTrace(ex, "Skipped process {ProcessId} due to access error", p.Id);
|
|
}
|
|
}
|
|
|
|
_logger.LogDebug("Processed {ValidCount} valid processes, skipped {SkippedCount}", validProcesses.Count, skippedCount);
|
|
|
|
// Clean up old process entries to prevent memory leaks
|
|
CleanupOldProcessEntries(validProcesses.Select(p => p.ProcessId).ToHashSet());
|
|
|
|
var topProcesses = validProcesses
|
|
.OrderByDescending(p => p.CpuUsage)
|
|
.ThenByDescending(p => p.MemoryUsage)
|
|
.Take(count)
|
|
.ToList();
|
|
|
|
_logger.LogDebug("Returning {TopCount} top processes", topProcesses.Count);
|
|
return topProcesses;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting top processes");
|
|
return new List<ProcessInfo>();
|
|
}
|
|
}
|
|
|
|
private async Task<float> GetCpuTemperatureAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
// Try to get CPU temperature from WMI
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
using var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSAcpi_ThermalZoneTemperature");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
var temp = Convert.ToDouble(obj["CurrentTemperature"]);
|
|
return (float)((temp - 2732) / 10.0); // Convert from tenths of Kelvin to Celsius
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
return 0f;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("cpu_temperature", ex, "Could not get CPU temperature");
|
|
return 0f;
|
|
}
|
|
}
|
|
|
|
private async Task<TemperatureInfo> GetTemperatureInfoAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var temperatureInfo = new TemperatureInfo
|
|
{
|
|
CPU = GetCpuTemperatureAsync().Result,
|
|
HardDrives = new List<HardDriveTemp>()
|
|
};
|
|
|
|
// Get hard drive temperatures using improved method
|
|
try
|
|
{
|
|
var drives = DriveInfo.GetDrives();
|
|
foreach (var drive in drives.Where(d => d.IsReady && d.DriveType == DriveType.Fixed))
|
|
{
|
|
var hardDriveTemp = new HardDriveTemp
|
|
{
|
|
Drive = drive.Name,
|
|
Temperature = 0f,
|
|
Health = "Unknown"
|
|
};
|
|
|
|
// Use the same SMART data access as in GetDiskTemperature
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject disk in collection)
|
|
{
|
|
var model = disk["Model"]?.ToString() ?? "";
|
|
|
|
// Try to get SMART data using MSStorageDriver_ATAPISmartData
|
|
try
|
|
{
|
|
using var smartSearcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
|
|
using var smartCollection = smartSearcher.Get();
|
|
foreach (ManagementObject smartData in smartCollection)
|
|
{
|
|
var instanceName = smartData["InstanceName"]?.ToString() ?? "";
|
|
if (instanceName.Contains(drive.Name.Replace("\\", "").Replace(":", "")))
|
|
{
|
|
var vendorSpecific = smartData["VendorSpecific"] as byte[];
|
|
if (vendorSpecific != null && vendorSpecific.Length >= 362)
|
|
{
|
|
// Parse SMART attributes for temperature (attribute 194)
|
|
for (int i = 2; i < 362; i += 12)
|
|
{
|
|
if (i + 11 < vendorSpecific.Length && vendorSpecific[i] == 194)
|
|
{
|
|
hardDriveTemp.Temperature = vendorSpecific[i + 5];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parse SMART attributes for health indicators
|
|
bool hasWarnings = false;
|
|
for (int i = 2; i < 362; i += 12)
|
|
{
|
|
if (i + 11 < vendorSpecific.Length)
|
|
{
|
|
var attributeId = vendorSpecific[i];
|
|
var threshold = vendorSpecific[i + 2];
|
|
var value = vendorSpecific[i + 3];
|
|
|
|
// Check critical attributes
|
|
if ((attributeId == 5 || attributeId == 196 || attributeId == 197 || attributeId == 198) && value <= threshold)
|
|
{
|
|
hasWarnings = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
hardDriveTemp.Health = hasWarnings ? "Warning" : "Good";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"smart_temp_info_{drive.Name}", ex, $"Could not get SMART temperature info for {drive.Name}");
|
|
}
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"hdd_temp_info_{drive.Name}", ex, $"Could not get temperature info for drive {drive.Name}");
|
|
}
|
|
|
|
temperatureInfo.HardDrives.Add(hardDriveTemp);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("hdd_temperature", ex, "Could not get hard drive temperatures");
|
|
}
|
|
|
|
return temperatureInfo;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting temperature information");
|
|
return new TemperatureInfo();
|
|
}
|
|
}
|
|
|
|
private string GetProcessCommandLine(int processId)
|
|
{
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
using var searcher = new ManagementObjectSearcher($"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {processId}");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
return obj["CommandLine"]?.ToString() ?? "";
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
return "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private float CalculateProcessCpuUsage(int processId, TimeSpan currentProcessorTime, DateTime currentTime)
|
|
{
|
|
try
|
|
{
|
|
// Check if we have previous data for this process
|
|
if (_previousProcessorTimes.TryGetValue(processId, out var previousData))
|
|
{
|
|
var timeDifference = currentTime - previousData.Timestamp;
|
|
var processorTimeDifference = currentProcessorTime - previousData.ProcessorTime;
|
|
|
|
// Avoid division by zero and ensure meaningful time has passed
|
|
if (timeDifference.TotalMilliseconds > 100 && processorTimeDifference.TotalMilliseconds >= 0)
|
|
{
|
|
// Calculate CPU usage percentage
|
|
// ProcessorTime is the total time the process has used the CPU
|
|
// We need to calculate how much of the elapsed time was spent using CPU
|
|
var cpuUsagePercent = (processorTimeDifference.TotalMilliseconds / timeDifference.TotalMilliseconds) * 100.0;
|
|
|
|
// Account for multiple cores - divide by number of cores to get a percentage relative to total system
|
|
cpuUsagePercent = cpuUsagePercent / Environment.ProcessorCount;
|
|
|
|
// Store current data for next calculation
|
|
_previousProcessorTimes[processId] = (currentProcessorTime, currentTime);
|
|
|
|
return Math.Min((float)cpuUsagePercent, 100.0f); // Cap at 100%
|
|
}
|
|
}
|
|
|
|
// Store current data for next calculation (first time seeing this process or insufficient time passed)
|
|
_previousProcessorTimes[processId] = (currentProcessorTime, currentTime);
|
|
return 0f; // Can't calculate on first measurement
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning($"cpu_calc_{processId}", ex, $"Error calculating CPU usage for process {processId}");
|
|
return 0f;
|
|
}
|
|
}
|
|
|
|
private void CleanupOldProcessEntries(HashSet<int> currentProcessIds)
|
|
{
|
|
try
|
|
{
|
|
// Remove entries for processes that no longer exist
|
|
var keysToRemove = _previousProcessorTimes.Keys
|
|
.Where(pid => !currentProcessIds.Contains(pid))
|
|
.ToList();
|
|
|
|
foreach (var key in keysToRemove)
|
|
{
|
|
_previousProcessorTimes.Remove(key);
|
|
}
|
|
|
|
// Also remove very old entries (older than 5 minutes) to prevent indefinite growth
|
|
var cutoffTime = DateTime.Now.AddMinutes(-5);
|
|
var oldKeys = _previousProcessorTimes
|
|
.Where(kvp => kvp.Value.Timestamp < cutoffTime)
|
|
.Select(kvp => kvp.Key)
|
|
.ToList();
|
|
|
|
foreach (var key in oldKeys)
|
|
{
|
|
_previousProcessorTimes.Remove(key);
|
|
}
|
|
|
|
if (keysToRemove.Count > 0 || oldKeys.Count > 0)
|
|
{
|
|
_logger.LogTrace("Cleaned up {RemovedCount} old process entries, {OldCount} expired entries",
|
|
keysToRemove.Count, oldKeys.Count);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("cleanup_process", ex, "Error cleaning up old process entries");
|
|
}
|
|
}
|
|
|
|
private ulong GetTotalSystemMemory()
|
|
{
|
|
try
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
using var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
|
|
using var collection = searcher.Get();
|
|
foreach (ManagementObject obj in collection)
|
|
{
|
|
return (ulong)obj["TotalPhysicalMemory"];
|
|
}
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
return 0UL;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogSuppressedWarning("total_memory", ex, "Could not get total system memory");
|
|
return 0UL;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
foreach (var counter in _counters.Values)
|
|
{
|
|
counter?.Dispose();
|
|
}
|
|
_counters.Clear();
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
}
|
|
}
|
|
}
|