Add start-service.bat script for Resource Monitor Service v2.0
- Introduced a batch script to simplify the startup process for the Resource Monitor Service. - Included checks for .NET 9.0 Runtime installation. - Added build and run commands for the service with appropriate error handling. - Provided user instructions and API documentation links in the script output.
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ResourceMonitorService.Configuration;
|
||||
using ResourceMonitorService.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ResourceMonitorService.Services
|
||||
{
|
||||
public interface IAlertService
|
||||
{
|
||||
Task CheckAndGenerateAlertsAsync(ResourceUsage resourceUsage);
|
||||
Task<List<Alert>> GetActiveAlertsAsync();
|
||||
Task<List<Alert>> GetAlertHistoryAsync(int count = 100);
|
||||
Task ResolveAlertAsync(string alertId);
|
||||
Task<bool> IsAlertingEnabledAsync();
|
||||
event EventHandler<Alert>? AlertTriggered;
|
||||
event EventHandler<Alert>? AlertResolved;
|
||||
}
|
||||
|
||||
public class AlertService : IAlertService
|
||||
{
|
||||
private readonly ILogger<AlertService> _logger;
|
||||
private readonly MonitoringSettings _settings;
|
||||
private readonly ConcurrentDictionary<string, Alert> _activeAlerts;
|
||||
private readonly ConcurrentQueue<Alert> _alertHistory;
|
||||
private readonly Dictionary<string, DateTime> _lastAlertTime;
|
||||
private readonly Dictionary<string, DateTime> _thresholdExceededTime;
|
||||
|
||||
public event EventHandler<Alert>? AlertTriggered;
|
||||
public event EventHandler<Alert>? AlertResolved;
|
||||
|
||||
public AlertService(ILogger<AlertService> logger, IOptions<MonitoringSettings> settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings.Value;
|
||||
_activeAlerts = new ConcurrentDictionary<string, Alert>();
|
||||
_alertHistory = new ConcurrentQueue<Alert>();
|
||||
_lastAlertTime = new Dictionary<string, DateTime>();
|
||||
_thresholdExceededTime = new Dictionary<string, DateTime>();
|
||||
}
|
||||
|
||||
public async Task CheckAndGenerateAlertsAsync(ResourceUsage resourceUsage)
|
||||
{
|
||||
if (!_settings.EnableAlerts)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// Check CPU usage
|
||||
CheckThreshold("CPU", resourceUsage.CPU.Usage, "CPU Usage", "%");
|
||||
|
||||
// Check CPU temperature
|
||||
if (resourceUsage.CPU.Temperature > 0)
|
||||
CheckThreshold("CPUTemp", resourceUsage.CPU.Temperature, "CPU Temperature", "°C");
|
||||
|
||||
// Check Memory usage
|
||||
CheckThreshold("Memory", resourceUsage.Memory.UsagePercentage, "Memory Usage", "%");
|
||||
|
||||
// Check GPU usage
|
||||
if (resourceUsage.GPU.IsAvailable)
|
||||
{
|
||||
CheckThreshold("GPU", resourceUsage.GPU.Usage, "GPU Usage", "%");
|
||||
if (resourceUsage.GPU.Temperature > 0)
|
||||
CheckThreshold("GPUTemp", resourceUsage.GPU.Temperature, "GPU Temperature", "°C");
|
||||
}
|
||||
|
||||
// Check disk usage
|
||||
foreach (var disk in resourceUsage.Disks)
|
||||
{
|
||||
CheckThreshold($"Disk_{disk.DriveLetter}", disk.UsagePercentage,
|
||||
$"Disk Usage ({disk.DriveLetter})", "%");
|
||||
|
||||
if (disk.DiskTime > 0)
|
||||
CheckThreshold($"DiskTime_{disk.DriveLetter}", disk.DiskTime,
|
||||
$"Disk Time ({disk.DriveLetter})", "%");
|
||||
}
|
||||
|
||||
// Check for processes using too much memory
|
||||
var topMemoryProcess = resourceUsage.TopProcesses
|
||||
.OrderByDescending(p => p.MemoryUsage)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (topMemoryProcess != null)
|
||||
{
|
||||
var memoryUsageGB = topMemoryProcess.MemoryUsage / (1024.0 * 1024.0 * 1024.0);
|
||||
if (memoryUsageGB > 4) // Alert if a single process is using more than 4GB
|
||||
{
|
||||
CheckCustomAlert($"ProcessMemory_{topMemoryProcess.Name}",
|
||||
(float)memoryUsageGB, 4f, 8f,
|
||||
$"High Memory Usage - {topMemoryProcess.Name}", "GB");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve alerts that are no longer active
|
||||
ResolveInactiveAlerts(resourceUsage);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking and generating alerts");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckThreshold(string component, float currentValue, string description, string unit)
|
||||
{
|
||||
var threshold = _settings.AlertThresholds.FirstOrDefault(t =>
|
||||
t.Component.Equals(component, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (threshold == null || !threshold.IsEnabled)
|
||||
return;
|
||||
|
||||
CheckCustomAlert(component, currentValue, threshold.WarningThreshold,
|
||||
threshold.CriticalThreshold, description, unit, TimeSpan.FromSeconds(threshold.DurationSeconds));
|
||||
}
|
||||
|
||||
private void CheckCustomAlert(string component, float currentValue, float warningThreshold,
|
||||
float criticalThreshold, string description, string unit, TimeSpan? duration = null)
|
||||
{
|
||||
var alertDuration = duration ?? TimeSpan.FromSeconds(30);
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Determine alert level
|
||||
string? alertLevel = null;
|
||||
float thresholdValue = 0;
|
||||
|
||||
if (currentValue >= criticalThreshold)
|
||||
{
|
||||
alertLevel = "Critical";
|
||||
thresholdValue = criticalThreshold;
|
||||
}
|
||||
else if (currentValue >= warningThreshold)
|
||||
{
|
||||
alertLevel = "Warning";
|
||||
thresholdValue = warningThreshold;
|
||||
}
|
||||
|
||||
if (alertLevel != null)
|
||||
{
|
||||
// Check if threshold has been exceeded for the required duration
|
||||
var key = $"{component}_{alertLevel}";
|
||||
|
||||
if (!_thresholdExceededTime.ContainsKey(key))
|
||||
{
|
||||
_thresholdExceededTime[key] = now;
|
||||
return; // Not exceeded long enough yet
|
||||
}
|
||||
|
||||
var exceededDuration = now - _thresholdExceededTime[key];
|
||||
if (exceededDuration < alertDuration)
|
||||
return; // Not exceeded long enough yet
|
||||
|
||||
// Check if we've already sent this alert recently (avoid spam)
|
||||
if (_lastAlertTime.TryGetValue(key, out var lastAlert))
|
||||
{
|
||||
if (now - lastAlert < TimeSpan.FromMinutes(5))
|
||||
return; // Too soon since last alert
|
||||
}
|
||||
|
||||
// Create and trigger alert
|
||||
var alert = new Alert
|
||||
{
|
||||
Timestamp = now,
|
||||
Component = component,
|
||||
Level = alertLevel,
|
||||
Message = $"{description} is {alertLevel.ToLower()}: {currentValue:F1}{unit} (threshold: {thresholdValue:F1}{unit})",
|
||||
CurrentValue = currentValue,
|
||||
ThresholdValue = thresholdValue,
|
||||
IsResolved = false
|
||||
};
|
||||
|
||||
var alertId = $"{component}_{alertLevel}_{now:yyyyMMddHHmmss}";
|
||||
_activeAlerts[alertId] = alert;
|
||||
_alertHistory.Enqueue(alert);
|
||||
_lastAlertTime[key] = now;
|
||||
|
||||
// Trim history if too large
|
||||
while (_alertHistory.Count > 1000)
|
||||
{
|
||||
_alertHistory.TryDequeue(out _);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Alert triggered: {Message}", alert.Message);
|
||||
AlertTriggered?.Invoke(this, alert);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Value is below threshold, remove tracking
|
||||
var warningKey = $"{component}_Warning";
|
||||
var criticalKey = $"{component}_Critical";
|
||||
_thresholdExceededTime.Remove(warningKey);
|
||||
_thresholdExceededTime.Remove(criticalKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveInactiveAlerts(ResourceUsage resourceUsage)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var alertsToResolve = new List<string>();
|
||||
|
||||
foreach (var activeAlert in _activeAlerts)
|
||||
{
|
||||
var alert = activeAlert.Value;
|
||||
var shouldResolve = false;
|
||||
|
||||
// Check if the condition that triggered the alert is no longer true
|
||||
switch (alert.Component)
|
||||
{
|
||||
case "CPU":
|
||||
shouldResolve = resourceUsage.CPU.Usage < alert.ThresholdValue;
|
||||
break;
|
||||
case "CPUTemp":
|
||||
shouldResolve = resourceUsage.CPU.Temperature < alert.ThresholdValue;
|
||||
break;
|
||||
case "Memory":
|
||||
shouldResolve = resourceUsage.Memory.UsagePercentage < alert.ThresholdValue;
|
||||
break;
|
||||
case "GPU":
|
||||
shouldResolve = !resourceUsage.GPU.IsAvailable || resourceUsage.GPU.Usage < alert.ThresholdValue;
|
||||
break;
|
||||
case "GPUTemp":
|
||||
shouldResolve = !resourceUsage.GPU.IsAvailable || resourceUsage.GPU.Temperature < alert.ThresholdValue;
|
||||
break;
|
||||
default:
|
||||
// For disk alerts and others, check if component still exists and is below threshold
|
||||
if (alert.Component.StartsWith("Disk_"))
|
||||
{
|
||||
var driveLetter = alert.Component.Replace("Disk_", "").Replace("DiskTime_", "");
|
||||
var disk = resourceUsage.Disks.FirstOrDefault(d => d.DriveLetter.Contains(driveLetter));
|
||||
if (disk != null)
|
||||
{
|
||||
shouldResolve = alert.Component.StartsWith("DiskTime_")
|
||||
? disk.DiskTime < alert.ThresholdValue
|
||||
: disk.UsagePercentage < alert.ThresholdValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldResolve = true; // Disk no longer available
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Auto-resolve old alerts (older than 1 hour)
|
||||
if (now - alert.Timestamp > TimeSpan.FromHours(1))
|
||||
{
|
||||
shouldResolve = true;
|
||||
}
|
||||
|
||||
if (shouldResolve)
|
||||
{
|
||||
alertsToResolve.Add(activeAlert.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve alerts
|
||||
foreach (var alertId in alertsToResolve)
|
||||
{
|
||||
if (_activeAlerts.TryRemove(alertId, out var resolvedAlert))
|
||||
{
|
||||
resolvedAlert.IsResolved = true;
|
||||
resolvedAlert.ResolvedAt = now;
|
||||
|
||||
_logger.LogInformation("Alert resolved: {Message}", resolvedAlert.Message);
|
||||
AlertResolved?.Invoke(this, resolvedAlert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Alert>> GetActiveAlertsAsync()
|
||||
{
|
||||
return await Task.FromResult(_activeAlerts.Values.ToList());
|
||||
}
|
||||
|
||||
public async Task<List<Alert>> GetAlertHistoryAsync(int count = 100)
|
||||
{
|
||||
return await Task.FromResult(_alertHistory.TakeLast(count).ToList());
|
||||
}
|
||||
|
||||
public async Task ResolveAlertAsync(string alertId)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (_activeAlerts.TryRemove(alertId, out var alert))
|
||||
{
|
||||
alert.IsResolved = true;
|
||||
alert.ResolvedAt = DateTime.Now;
|
||||
|
||||
_logger.LogInformation("Alert manually resolved: {Message}", alert.Message);
|
||||
AlertResolved?.Invoke(this, alert);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> IsAlertingEnabledAsync()
|
||||
{
|
||||
return await Task.FromResult(_settings.EnableAlerts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ResourceMonitorService.Configuration;
|
||||
using ResourceMonitorService.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace ResourceMonitorService.Services
|
||||
{
|
||||
public interface IGameDetectionService
|
||||
{
|
||||
Task<GameInfo?> GetCurrentlyRunningGameAsync();
|
||||
Task<List<GameInfo>> GetAllDetectedGamesAsync();
|
||||
Task<bool> IsGameRunningFullscreenAsync();
|
||||
Task<float> GetGameFpsAsync(string processName);
|
||||
}
|
||||
|
||||
public class GameDetectionService : IGameDetectionService
|
||||
{
|
||||
private readonly ILogger<GameDetectionService> _logger;
|
||||
private readonly MonitoringSettings _settings;
|
||||
|
||||
// Windows API imports for fullscreen detection
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
public GameDetectionService(ILogger<GameDetectionService> logger, IOptions<MonitoringSettings> settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
public async Task<GameInfo?> GetCurrentlyRunningGameAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var processes = Process.GetProcesses();
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (process.MainModule?.FileName == null)
|
||||
continue;
|
||||
|
||||
var filePath = process.MainModule.FileName;
|
||||
var gameInfo = DetectGameFromPath(filePath, process);
|
||||
|
||||
if (gameInfo != null)
|
||||
{
|
||||
gameInfo.IsFullscreen = IsGameRunningFullscreenAsync().Result;
|
||||
gameInfo.FPS = GetGameFpsAsync(process.ProcessName).Result;
|
||||
return gameInfo;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle access exceptions silently - some processes can't be accessed
|
||||
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting currently running game");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<GameInfo>> GetAllDetectedGamesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var games = new List<GameInfo>();
|
||||
var processes = Process.GetProcesses();
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (process.MainModule?.FileName == null)
|
||||
continue;
|
||||
|
||||
var filePath = process.MainModule.FileName;
|
||||
var gameInfo = DetectGameFromPath(filePath, process);
|
||||
|
||||
if (gameInfo != null)
|
||||
{
|
||||
games.Add(gameInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
|
||||
}
|
||||
}
|
||||
|
||||
return games;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting all detected games");
|
||||
return new List<GameInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsGameRunningFullscreenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
if (!IsWindowVisible(foregroundWindow))
|
||||
return false;
|
||||
|
||||
if (!GetWindowRect(foregroundWindow, out RECT rect))
|
||||
return false;
|
||||
|
||||
// Get screen dimensions
|
||||
var screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
|
||||
var screenHeight = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;
|
||||
|
||||
// Check if window covers the entire screen
|
||||
var windowWidth = rect.Right - rect.Left;
|
||||
var windowHeight = rect.Bottom - rect.Top;
|
||||
|
||||
return windowWidth >= screenWidth && windowHeight >= screenHeight;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine if game is running fullscreen");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<float> GetGameFpsAsync(string processName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// This is a simplified FPS detection - in reality, you'd need more sophisticated methods
|
||||
// such as hooking into DirectX/OpenGL or using external tools like RTSS
|
||||
|
||||
// For now, we'll return 0 as a placeholder
|
||||
// In a real implementation, you might:
|
||||
// 1. Use Windows Performance Toolkit (WPT) ETW events
|
||||
// 2. Hook into D3D11/D3D12 present calls
|
||||
// 3. Use NVIDIA's NVAPI or AMD's ADL
|
||||
// 4. Parse log files from games that output FPS
|
||||
|
||||
return 0f;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get FPS for process {ProcessName}", processName);
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private GameInfo? DetectGameFromPath(string filePath, Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check each configured game platform path
|
||||
foreach (var platformPath in _settings.GamePlatformPaths)
|
||||
{
|
||||
if (filePath.Contains(platformPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var platform = GetPlatformFromPath(platformPath);
|
||||
var gameName = ExtractGameNameFromPath(filePath, platformPath);
|
||||
|
||||
return new GameInfo
|
||||
{
|
||||
GameName = gameName,
|
||||
ExecutableName = Path.GetFileName(filePath),
|
||||
FullPath = filePath,
|
||||
ProcessId = process.Id,
|
||||
MemoryUsage = (ulong)process.WorkingSet64,
|
||||
CpuTime = process.TotalProcessorTime,
|
||||
StartTime = process.StartTime,
|
||||
Platform = platform,
|
||||
IsFullscreen = false, // Will be set by caller
|
||||
FPS = 0f // Will be set by caller
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Additional checks for common game launchers and executables
|
||||
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
|
||||
var knownGameExecutables = new[]
|
||||
{
|
||||
"game", "launcher", "client", "main", "start", "run",
|
||||
// Add more common game executable patterns
|
||||
};
|
||||
|
||||
var gameIndicators = new[]
|
||||
{
|
||||
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
|
||||
"steam", "epic", "origin", "uplay", "battle.net"
|
||||
};
|
||||
|
||||
// Check if it's likely a game based on executable name or path
|
||||
if (knownGameExecutables.Any(exe => fileName.Contains(exe)) ||
|
||||
gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Try to determine platform and game name from other indicators
|
||||
var platform = DeterminePlatformFromProcess(process, filePath);
|
||||
var gameName = DetermineGameNameFromProcess(process, filePath);
|
||||
|
||||
if (!string.IsNullOrEmpty(gameName))
|
||||
{
|
||||
return new GameInfo
|
||||
{
|
||||
GameName = gameName,
|
||||
ExecutableName = Path.GetFileName(filePath),
|
||||
FullPath = filePath,
|
||||
ProcessId = process.Id,
|
||||
MemoryUsage = (ulong)process.WorkingSet64,
|
||||
CpuTime = process.TotalProcessorTime,
|
||||
StartTime = process.StartTime,
|
||||
Platform = platform,
|
||||
IsFullscreen = false,
|
||||
FPS = 0f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error detecting game from path {FilePath}", filePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPlatformFromPath(string platformPath)
|
||||
{
|
||||
return platformPath.ToLowerInvariant() switch
|
||||
{
|
||||
var path when path.Contains("steamapps") => "Steam",
|
||||
var path when path.Contains("epic games") => "Epic Games Store",
|
||||
var path when path.Contains("gog galaxy") => "GOG Galaxy",
|
||||
var path when path.Contains("origin games") => "EA Origin",
|
||||
var path when path.Contains("ubisoft game launcher") => "Ubisoft Connect",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
|
||||
private string ExtractGameNameFromPath(string filePath, string platformPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = filePath.Split(new[] { platformPath }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var gamePath = parts[1];
|
||||
var gameFolder = gamePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
return gameFolder;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not extract game name from path {FilePath}", filePath);
|
||||
}
|
||||
|
||||
return Path.GetFileNameWithoutExtension(filePath);
|
||||
}
|
||||
|
||||
private string DeterminePlatformFromProcess(Process process, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check parent processes for launcher indicators
|
||||
var currentProcess = process;
|
||||
for (int i = 0; i < 3; i++) // Check up to 3 levels up
|
||||
{
|
||||
try
|
||||
{
|
||||
var parentId = GetParentProcessId(currentProcess.Id);
|
||||
if (parentId == 0) break;
|
||||
|
||||
var parentProcess = Process.GetProcessById(parentId);
|
||||
var parentName = parentProcess.ProcessName.ToLowerInvariant();
|
||||
|
||||
if (parentName.Contains("steam"))
|
||||
return "Steam";
|
||||
if (parentName.Contains("epic"))
|
||||
return "Epic Games Store";
|
||||
if (parentName.Contains("origin"))
|
||||
return "EA Origin";
|
||||
if (parentName.Contains("uplay") || parentName.Contains("ubisoft"))
|
||||
return "Ubisoft Connect";
|
||||
if (parentName.Contains("gog"))
|
||||
return "GOG Galaxy";
|
||||
|
||||
currentProcess = parentProcess;
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check file path for platform indicators
|
||||
if (filePath.Contains("Program Files (x86)"))
|
||||
return "Windows Store/Other";
|
||||
if (filePath.Contains("WindowsApps"))
|
||||
return "Microsoft Store";
|
||||
|
||||
return "Standalone";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine platform for process {ProcessName}", process.ProcessName);
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private string DetermineGameNameFromProcess(Process process, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get a meaningful name from various sources
|
||||
|
||||
// 1. Try from file properties
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
||||
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
||||
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return versionInfo.ProductName;
|
||||
}
|
||||
|
||||
// 2. Try from directory name
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
var directoryName = Path.GetFileName(directory);
|
||||
if (!string.IsNullOrEmpty(directoryName) &&
|
||||
!directoryName.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
|
||||
!directoryName.Equals("exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return directoryName;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to executable name
|
||||
return Path.GetFileNameWithoutExtension(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine game name for process {ProcessName}", process.ProcessName);
|
||||
return process.ProcessName;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetParentProcessId(int processId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new System.Management.ManagementObjectSearcher(
|
||||
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}");
|
||||
using var collection = searcher.Get();
|
||||
foreach (System.Management.ManagementObject obj in collection)
|
||||
{
|
||||
return Convert.ToInt32(obj["ParentProcessId"]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "Could not get parent process ID for {ProcessId}", processId);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
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<NetworkUsage> GetNetworkUsageAsync();
|
||||
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> _previousNetworkBytes;
|
||||
private readonly Dictionary<string, DateTime> _previousNetworkTime;
|
||||
|
||||
public ResourceMonitorService(ILogger<ResourceMonitorService> logger, IOptions<MonitoringSettings> settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings.Value;
|
||||
_counters = new Dictionary<string, PerformanceCounter>();
|
||||
_previousNetworkBytes = new Dictionary<string, long>();
|
||||
_previousNetworkTime = new Dictionary<string, DateTime>();
|
||||
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.EnableNetworkMonitoring)
|
||||
{
|
||||
_counters["network_bytes_sent"] = new PerformanceCounter("Network Interface", "Bytes Sent/sec", "*");
|
||||
_counters["network_bytes_received"] = new PerformanceCounter("Network Interface", "Bytes Received/sec", "*");
|
||||
}
|
||||
|
||||
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
|
||||
foreach (var counter in _counters.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
counter.NextValue();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize performance counter: {CounterName}", counter.CounterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize performance counters");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ResourceUsage> GetResourceUsageAsync()
|
||||
{
|
||||
var timestamp = DateTime.Now;
|
||||
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
Task.Run(async () => await GetCpuUsageAsync()),
|
||||
Task.Run(async () => await GetMemoryUsageAsync())
|
||||
};
|
||||
|
||||
if (_settings.EnableGpuMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetGpuUsageAsync()));
|
||||
|
||||
if (_settings.EnableDiskMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetDiskUsageAsync()));
|
||||
|
||||
if (_settings.EnableNetworkMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetNetworkUsageAsync()));
|
||||
|
||||
if (_settings.EnableProcessMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetTopProcessesAsync(_settings.MaxProcessesToTrack)));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return new ResourceUsage
|
||||
{
|
||||
Timestamp = timestamp,
|
||||
CPU = await GetCpuUsageAsync(),
|
||||
Memory = await GetMemoryUsageAsync(),
|
||||
GPU = _settings.EnableGpuMonitoring ? await GetGpuUsageAsync() : new GpuUsage(),
|
||||
Disks = _settings.EnableDiskMonitoring ? await GetDiskUsageAsync() : new List<DiskUsage>(),
|
||||
Network = _settings.EnableNetworkMonitoring ? await GetNetworkUsageAsync() : new NetworkUsage(),
|
||||
TopProcesses = _settings.EnableProcessMonitoring ? await GetTopProcessesAsync(_settings.MaxProcessesToTrack) : new List<ProcessInfo>(),
|
||||
Temperature = _settings.EnableTemperatureMonitoring ? await GetTemperatureInfoAsync() : new TemperatureInfo()
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
var coreUsages = new List<float>();
|
||||
for (int i = 0; i < Environment.ProcessorCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var coreCounter = new PerformanceCounter("Processor", "% Processor Time", i.ToString());
|
||||
coreCounter.NextValue();
|
||||
Thread.Sleep(100); // Small delay for accurate reading
|
||||
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;
|
||||
NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed);
|
||||
|
||||
// Try to get additional information
|
||||
var name = "NVIDIA GPU";
|
||||
var driverVersion = "Unknown";
|
||||
var memoryTotal = 0UL;
|
||||
var memoryUsed = 0UL;
|
||||
var powerUsage = 0U;
|
||||
|
||||
try
|
||||
{
|
||||
// Get GPU name and memory info 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)
|
||||
{
|
||||
name = obj["Name"]?.ToString() ?? name;
|
||||
driverVersion = obj["DriverVersion"]?.ToString() ?? driverVersion;
|
||||
if (obj["AdapterRAM"] != null)
|
||||
{
|
||||
memoryTotal = (ulong)obj["AdapterRAM"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(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,
|
||||
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 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
|
||||
try
|
||||
{
|
||||
var diskName = drive.Name.Replace("\\", "").Replace(":", "");
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var readCounter = new PerformanceCounter("LogicalDisk", "Disk Read Bytes/sec", diskName);
|
||||
using var writeCounter = new PerformanceCounter("LogicalDisk", "Disk Write Bytes/sec", diskName);
|
||||
using var timeCounter = new PerformanceCounter("LogicalDisk", "% Disk Time", diskName);
|
||||
|
||||
readCounter.NextValue();
|
||||
writeCounter.NextValue();
|
||||
timeCounter.NextValue();
|
||||
|
||||
Thread.Sleep(1000);
|
||||
|
||||
diskUsage.ReadSpeed = readCounter.NextValue() / (1024 * 1024); // Convert to MB/s
|
||||
diskUsage.WriteSpeed = writeCounter.NextValue() / (1024 * 1024); // Convert to MB/s
|
||||
diskUsage.DiskTime = timeCounter.NextValue();
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get disk performance data for drive {Drive}", drive.Name);
|
||||
}
|
||||
|
||||
// Try to determine if it's an SSD
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher($"SELECT * FROM Win32_LogicalDisk WHERE DeviceID='{drive.Name.TrimEnd('\\')}'");
|
||||
using var collection = searcher.Get();
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
// This is a simplified check; more sophisticated detection would be needed
|
||||
var mediaType = obj["MediaType"]?.ToString();
|
||||
diskUsage.IsSSD = mediaType?.Contains("SSD") == true || mediaType?.Contains("Solid") == true;
|
||||
break;
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine disk type for drive {Drive}", drive.Name);
|
||||
}
|
||||
|
||||
diskUsages.Add(diskUsage);
|
||||
}
|
||||
}
|
||||
|
||||
return diskUsages;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting disk usage");
|
||||
return new List<DiskUsage>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<NetworkUsage> GetNetworkUsageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var networkUsage = new NetworkUsage();
|
||||
var adapters = new List<NetworkAdapter>();
|
||||
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PerfRawData_Tcpip_NetworkInterface WHERE Name != 'Loopback'");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
var name = obj["Name"]?.ToString() ?? "";
|
||||
if (name.Contains("Loopback") || name.Contains("Isatap") || name.Contains("Teredo"))
|
||||
continue;
|
||||
|
||||
var bytesSent = Convert.ToInt64(obj["BytesSentPerSec"] ?? 0);
|
||||
var bytesReceived = Convert.ToInt64(obj["BytesReceivedPerSec"] ?? 0);
|
||||
var timestamp = DateTime.Now;
|
||||
|
||||
var adapter = new NetworkAdapter
|
||||
{
|
||||
Name = name,
|
||||
IsOperational = true
|
||||
};
|
||||
|
||||
// Calculate speeds if we have previous data
|
||||
var key = $"{name}_sent";
|
||||
if (_previousNetworkBytes.ContainsKey(key) && _previousNetworkTime.ContainsKey(key))
|
||||
{
|
||||
var timeDiff = (timestamp - _previousNetworkTime[key]).TotalSeconds;
|
||||
if (timeDiff > 0)
|
||||
{
|
||||
var bytesDiff = bytesSent - _previousNetworkBytes[key];
|
||||
adapter.UploadSpeed = (float)(bytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
||||
networkUsage.UploadSpeed += adapter.UploadSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
key = $"{name}_received";
|
||||
if (_previousNetworkBytes.ContainsKey(key) && _previousNetworkTime.ContainsKey(key))
|
||||
{
|
||||
var timeDiff = (timestamp - _previousNetworkTime[key]).TotalSeconds;
|
||||
if (timeDiff > 0)
|
||||
{
|
||||
var bytesDiff = bytesReceived - _previousNetworkBytes[key];
|
||||
adapter.DownloadSpeed = (float)(bytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
||||
networkUsage.DownloadSpeed += adapter.DownloadSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
// Store current values for next calculation
|
||||
_previousNetworkBytes[$"{name}_sent"] = bytesSent;
|
||||
_previousNetworkBytes[$"{name}_received"] = bytesReceived;
|
||||
_previousNetworkTime[$"{name}_sent"] = timestamp;
|
||||
_previousNetworkTime[$"{name}_received"] = timestamp;
|
||||
|
||||
networkUsage.BytesSent += (ulong)bytesSent;
|
||||
networkUsage.BytesReceived += (ulong)bytesReceived;
|
||||
|
||||
adapters.Add(adapter);
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get network performance data");
|
||||
}
|
||||
|
||||
networkUsage.Adapters = adapters;
|
||||
return networkUsage;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting network usage");
|
||||
return new NetworkUsage();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ProcessInfo>> GetTopProcessesAsync(int count = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var processes = Process.GetProcesses()
|
||||
.Where(p => !p.HasExited)
|
||||
.Select(p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new ProcessInfo
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.ProcessName,
|
||||
MemoryUsage = (ulong)p.WorkingSet64,
|
||||
ProcessorTime = p.TotalProcessorTime,
|
||||
StartTime = p.StartTime,
|
||||
ExecutablePath = p.MainModule?.FileName ?? "",
|
||||
CommandLine = GetProcessCommandLine(p.Id)
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null; // Skip processes that throw exceptions
|
||||
}
|
||||
})
|
||||
.Where(p => p != null)
|
||||
.OrderByDescending(p => p!.MemoryUsage)
|
||||
.Take(count)
|
||||
.Cast<ProcessInfo>()
|
||||
.ToList();
|
||||
|
||||
return processes;
|
||||
});
|
||||
}
|
||||
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)
|
||||
{
|
||||
_logger.LogWarning(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>()
|
||||
};
|
||||
|
||||
// Try to get hard drive temperatures
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
|
||||
using var collection = searcher.Get();
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
var instanceName = obj["InstanceName"]?.ToString() ?? "";
|
||||
// This would need more sophisticated parsing for actual SMART data
|
||||
temperatureInfo.HardDrives.Add(new HardDriveTemp
|
||||
{
|
||||
Drive = instanceName,
|
||||
Temperature = 0f, // Would need SMART data parsing
|
||||
Health = "Unknown"
|
||||
});
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(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 "";
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
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 ISystemInfoService
|
||||
{
|
||||
Task<SystemInfo> GetSystemInfoAsync();
|
||||
Task<bool> IsVirtualMachineAsync();
|
||||
Task<string> GetHypervisorVendorAsync();
|
||||
Task<DateTime> GetBootTimeAsync();
|
||||
Task<string> GetCpuNameAsync();
|
||||
}
|
||||
|
||||
public class SystemInfoService : ISystemInfoService
|
||||
{
|
||||
private readonly ILogger<SystemInfoService> _logger;
|
||||
private readonly MonitoringSettings _settings;
|
||||
private SystemInfo? _cachedSystemInfo;
|
||||
private DateTime _lastCacheUpdate = DateTime.MinValue;
|
||||
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5);
|
||||
|
||||
public SystemInfoService(ILogger<SystemInfoService> logger, IOptions<MonitoringSettings> settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
public async Task<SystemInfo> GetSystemInfoAsync()
|
||||
{
|
||||
if (_cachedSystemInfo != null && DateTime.Now - _lastCacheUpdate < _cacheExpiration)
|
||||
{
|
||||
_cachedSystemInfo.Uptime = DateTime.Now - _cachedSystemInfo.BootTime;
|
||||
return _cachedSystemInfo;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var systemInfo = new SystemInfo
|
||||
{
|
||||
MachineName = Environment.MachineName,
|
||||
OSVersion = System.Runtime.InteropServices.RuntimeInformation.OSDescription,
|
||||
OSArchitecture = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString(),
|
||||
ProcessorCount = Environment.ProcessorCount,
|
||||
TotalPhysicalMemory = await GetTotalPhysicalMemoryAsync(),
|
||||
CPUName = await GetCpuNameAsync(),
|
||||
BootTime = await GetBootTimeAsync(),
|
||||
Domain = Environment.UserDomainName,
|
||||
IsVirtualMachine = await IsVirtualMachineAsync(),
|
||||
HypervisorVendor = await GetHypervisorVendorAsync()
|
||||
};
|
||||
|
||||
systemInfo.Uptime = DateTime.Now - systemInfo.BootTime;
|
||||
|
||||
_cachedSystemInfo = systemInfo;
|
||||
_lastCacheUpdate = DateTime.Now;
|
||||
|
||||
return systemInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting system information");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsVirtualMachineAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// Check for common VM indicators
|
||||
var queries = new[]
|
||||
{
|
||||
"SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE '%VMware%' OR Manufacturer LIKE '%VirtualBox%' OR Manufacturer LIKE '%Microsoft Corporation%' OR Model LIKE '%Virtual%'",
|
||||
"SELECT * FROM Win32_BIOS WHERE SerialNumber LIKE '%VMware%' OR SerialNumber LIKE '%VirtualBox%' OR Version LIKE '%VBOX%'",
|
||||
"SELECT * FROM Win32_SystemEnclosure WHERE Manufacturer LIKE '%VMware%' OR Manufacturer LIKE '%VirtualBox%'"
|
||||
};
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher(query);
|
||||
using var collection = searcher.Get();
|
||||
if (collection.Count > 0)
|
||||
return true;
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine if running in virtual machine");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetHypervisorVendorAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem");
|
||||
using var collection = searcher.Get();
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
var manufacturer = obj["Manufacturer"]?.ToString() ?? "";
|
||||
var model = obj["Model"]?.ToString() ?? "";
|
||||
|
||||
if (manufacturer.Contains("VMware"))
|
||||
return "VMware";
|
||||
if (manufacturer.Contains("Microsoft Corporation") && model.Contains("Virtual"))
|
||||
return "Hyper-V";
|
||||
if (manufacturer.Contains("QEMU"))
|
||||
return "QEMU/KVM";
|
||||
if (manufacturer.Contains("VirtualBox"))
|
||||
return "VirtualBox";
|
||||
if (manufacturer.Contains("Xen"))
|
||||
return "Xen";
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
return "Unknown";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not determine hypervisor vendor");
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DateTime> GetBootTimeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher("SELECT LastBootUpTime FROM Win32_OperatingSystem");
|
||||
using var collection = searcher.Get();
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
var bootTime = obj["LastBootUpTime"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(bootTime))
|
||||
{
|
||||
return ManagementDateTimeConverter.ToDateTime(bootTime);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
return DateTime.Now.AddMilliseconds(-Environment.TickCount64);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get boot time from WMI, using tick count");
|
||||
return DateTime.Now.AddMilliseconds(-Environment.TickCount64);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetCpuNameAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_Processor");
|
||||
using var collection = searcher.Get();
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
return obj["Name"]?.ToString()?.Trim() ?? "Unknown CPU";
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
return "Unknown CPU";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get CPU name");
|
||||
return "Unknown CPU";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ulong> GetTotalPhysicalMemoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
#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 (ulong)0;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get total physical memory");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user