diff --git a/.gitignore b/.gitignore index 10f2ada..83d674f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Ignore all .log files *.log +# Ignore logs directory and log files +logs/ +*.txt + # Ignore all .tmp files *.tmp diff --git a/Configuration/MonitoringSettings.cs b/Configuration/MonitoringSettings.cs index d6a5a87..3fd27e9 100644 --- a/Configuration/MonitoringSettings.cs +++ b/Configuration/MonitoringSettings.cs @@ -23,6 +23,13 @@ namespace ResourceMonitorService.Configuration @"\Ubisoft Game Launcher\games\" }; + public List GameRootFolders { get; set; } = new() + { + @"C:\Games", + @"D:\Games", + @"E:\Games" + }; + public List AlertThresholds { get; set; } = new() { new() { Component = "CPU", WarningThreshold = 80, CriticalThreshold = 95, DurationSeconds = 30 }, diff --git a/Models/SystemInfo.cs b/Models/SystemInfo.cs index b585544..691f5e7 100644 --- a/Models/SystemInfo.cs +++ b/Models/SystemInfo.cs @@ -111,6 +111,7 @@ namespace ResourceMonitorService.Models public string Name { get; set; } = string.Empty; public float CpuUsage { get; set; } public ulong MemoryUsage { get; set; } + public float MemoryUsagePercentage { get; set; } public TimeSpan ProcessorTime { get; set; } public DateTime StartTime { get; set; } public string ExecutablePath { get; set; } = string.Empty; diff --git a/NvmlWrapper.cs b/NvmlWrapper.cs index 42297dc..63c8ab8 100644 --- a/NvmlWrapper.cs +++ b/NvmlWrapper.cs @@ -30,10 +30,27 @@ public static class NvmlWrapper [DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetFanSpeed")] public static extern int NvmlDeviceGetFanSpeed(IntPtr device, out uint speed); + [DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetMemoryInfo")] + public static extern int NvmlDeviceGetMemoryInfo(IntPtr device, out NvmlMemory memory); + + [DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetPowerUsage")] + public static extern int NvmlDeviceGetPowerUsage(IntPtr device, out uint power); + + [DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetName")] + public static extern int NvmlDeviceGetName(IntPtr device, byte[] name, uint length); + [StructLayout(LayoutKind.Sequential)] public struct NvmlUtilization { public uint Gpu; public uint Memory; } + + [StructLayout(LayoutKind.Sequential)] + public struct NvmlMemory + { + public ulong Total; + public ulong Free; + public ulong Used; + } } \ No newline at end of file diff --git a/README.md b/README.md index 708caff..5c313cd 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,424 @@ -# Resource Monitor Service v2.0 +# Resource Monitor Service for Unraid VM -A comprehensive Windows VM monitoring service designed specifically for Unraid virtual machines. This service provides real-time system monitoring, alerting, and remote management capabilities through a REST API. +A comprehensive system monitoring service specifically designed for Windows VMs running on Unraid servers. This service provides real-time monitoring of CPU, memory, GPU, disk, network, and system resources through a RESTful API. -## 🚀 New Features in v2.0 +## 🚀 Features -### Enhanced Monitoring -- **Multi-core CPU monitoring** with per-core usage and frequency tracking -- **Advanced memory monitoring** including paged/non-paged memory -- **Enhanced GPU monitoring** with NVIDIA GPU support via NVML -- **Comprehensive disk monitoring** with I/O performance metrics -- **Network monitoring** with per-adapter statistics -- **Temperature monitoring** for CPU, GPU, and storage devices -- **Process monitoring** with detailed resource usage tracking +### Core Monitoring +- **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection +- **Memory Monitoring**: RAM usage, available memory, committed memory, and paging +- **GPU Monitoring**: NVIDIA GPU usage, memory utilization, temperature, fan speed, and power consumption (via NVML) +- **Disk Monitoring**: I/O statistics, space usage, and performance counters +- **Network Monitoring**: Bandwidth usage, packet statistics, and interface data +- **Temperature Monitoring**: CPU and hard drive temperature sensors ### VM-Specific Features -- **Hypervisor detection** (VMware, Hyper-V, QEMU/KVM, etc.) -- **VM information** including boot time and uptime tracking -- **Virtual machine optimization** for better performance in virtualized environments +- **VM Detection**: Automatically detects virtualization environment +- **Hypervisor Identification**: Identifies VMware, VirtualBox, Hyper-V, KVM, etc. +- **Unraid Optimization**: Optimized for Unraid VM environments +- **Resource Alerting**: Configurable thresholds for resource usage alerts -### Game Detection & Management -- **Multi-platform game detection** (Steam, Epic Games, GOG, Origin, Ubisoft) -- **Fullscreen detection** for gaming sessions -- **Game performance monitoring** with memory and CPU usage -- **Enhanced process management** with graceful termination options +### Advanced Features +- **Game Detection**: Multi-platform game detection with fullscreen monitoring and configurable root folders +- **Process Management**: View top processes with CPU/memory percentages, terminate processes via API +- **Smart Alerting**: Duration-based alerting to prevent false positives +- **System Control**: Remote shutdown/restart capabilities +- **Health Monitoring**: Comprehensive health checks and uptime tracking +- **Real-time Metrics**: CPU usage calculation and memory percentage tracking for processes -### Intelligent Alerting System -- **Configurable thresholds** for CPU, memory, GPU, and temperature -- **Duration-based alerting** to prevent false positives -- **Alert history** and active alert management -- **Automatic alert resolution** when conditions improve +## 📡 API Endpoints -### Improved API -- **RESTful endpoints** with structured responses -- **Enhanced error handling** and logging -- **Configurable CORS** and API key authentication -- **Health check endpoints** for monitoring service status - - RAM Usage - - GPU Usage - - Currently Running Steam Games (if any) +The service runs on `http://localhost:5000` by default and provides the following endpoints: -- **Process Management**: Provides an API to kill processes by their process ID. +### System Information +- `GET /api/system-info` - Complete system information including VM details +- `GET /api/vm/info` - VM-specific information (hypervisor, uptime, etc.) +- `GET /api/health` - Service health status and monitoring capabilities +- `GET /api/metrics` - Service metrics and performance overview +- `GET /api/config` - Current configuration settings -## Directory Structure +### Resource Monitoring +- `GET /api/resource-usage` - Complete resource usage overview +- `GET /api/cpu-usage` - Detailed CPU metrics with per-core data +- `GET /api/memory-usage` - Memory utilization and statistics +- `GET /api/gpu-usage` - NVIDIA GPU usage, memory, temperature, fan speed, and power consumption +- `GET /api/disk-usage` - Disk I/O and space usage for all drives +- `GET /api/network-usage` - Network interface statistics +- `GET /api/top-processes?count=10` - Top processes by CPU/memory usage with percentage data -``` -ResourceUsageAPI/ -├── Worker.cs -├── Program.cs -├── Startup.cs -└── NvmlWrapper.cs +### Game Detection +- `GET /api/current-game` - Currently running game information +- `GET /api/all-games` - All detected games on the system +- `GET /api/fullscreen-status` - Check if any game is running fullscreen + +### Alerting System +- `GET /api/alerts/active` - Currently active alerts +- `GET /api/alerts/history?count=100` - Alert history +- `POST /api/alerts/{alertId}/resolve` - Manually resolve an alert +- `GET /api/alerts/enabled` - Check if alerting is enabled + +### System Control +- `POST /api/process/kill` - Terminate a process (requires process ID and optional force flag) +- `POST /api/system/shutdown` - Shutdown, restart, or cancel system operations +- `POST /api/service/stop` - Stop the monitoring service + +**Process Management Details:** +- The `/api/top-processes` endpoint returns processes sorted by CPU usage +- Each process includes real-time CPU usage percentage and memory usage percentage +- CPU usage is calculated using time-based measurements between API calls +- Memory usage percentage is calculated relative to total system memory +- Process termination supports both graceful (`force: false`) and forced (`force: true`) termination + +## 🛠️ Installation & Usage + +### Option 1: Console Application (Development/Testing) +```powershell +cd C:\Work\DEV\ResourceUsageAPI +dotnet run --configuration Release ``` -## Code Analysis +### Option 2: Windows Service (Production) +```powershell +# Run as Administrator +cd C:\Work\DEV\ResourceUsageAPI\publish +.\install-service.bat +``` -### Worker.cs +### Option 3: Standalone Executable +```powershell +cd C:\Work\DEV\ResourceUsageAPI\publish +.\ResourceMonitorService.exe +``` -This file contains the main logic for monitoring system resources and exposing APIs. +## ⚙️ Configuration -- **Dependencies**: Uses `System.Diagnostics`, `Microsoft.AspNetCore.Builder`, `Newtonsoft.Json` among others. -- **Methods**: - - `ExecuteAsync`: Sets up the ASP.NET Core web application, defines routes for resource usage and process management, and runs the server. - - `GetComputerInfo`: Retrieves basic system information. - - `GetCpuUsage`: Fetches CPU usage and lists top three processes by CPU usage if usage is over 80%. - - `GetRamUsage`: Calculates RAM usage percentage. - - `GetTotalPhysicalMemory`: Retrieves total physical memory size. - - `GetGpuUsage`: Uses NVIDIA Management Library (NVML) to fetch GPU usage, temperature, and fan speed. - - `GetCurrentlyRunningGame`: Detects if a Steam game is running by checking process paths. - - `GetCurrentTime`: Returns the current time. +Configuration is managed through `appsettings.json`: -### Program.cs +```json +{ + "MonitoringSettings": { + "UpdateIntervalMs": 5000, + "EnableGpuMonitoring": true, + "EnableDiskMonitoring": true, + "EnableNetworkMonitoring": true, + "EnableTemperatureMonitoring": true, + "EnableProcessMonitoring": true, + "EnableGameDetection": true, + "EnableAlerts": true, + "GamePlatformPaths": [ + "\\steamapps\\common\\", + "\\Epic Games\\", + "\\GOG Galaxy\\Games\\", + "\\Origin Games\\", + "\\Ubisoft Game Launcher\\games\\" + ], + "GameRootFolders": [ + "C:\\Games", + "D:\\Games", + "E:\\Games" + ] + }, + "AlertThresholds": { + "CpuUsageThreshold": 80.0, + "MemoryUsageThreshold": 85.0, + "GpuUsageThreshold": 90.0, + "DiskUsageThreshold": 90.0, + "TemperatureThreshold": 80.0, + "AlertDurationSeconds": 30 + }, + "ApiSettings": { + "RequireApiKey": false, + "AllowedOrigins": ["http://localhost:4200", "http://unraid:4200"], + "BasePath": "/api" + } +} +``` -This file sets up the hosting environment for the application. +### Game Detection Configuration -- **Dependencies**: Uses `Microsoft.Extensions.DependencyInjection` and `Microsoft.Extensions.Hosting`. -- **Methods**: - - `Main`: Entry point of the application, builds and runs the host. - - `CreateHostBuilder`: Configures services and determines if the application should run as a Windows service based on command-line arguments or environment variables. +The service supports advanced game detection through two complementary approaches: -### Startup.cs +#### **Platform-Based Detection** +Automatically detects games installed through popular game platforms: +- **Steam**: Games in `\steamapps\common\` directories +- **Epic Games Store**: Games in `\Epic Games\` directories +- **GOG Galaxy**: Games in `\GOG Galaxy\Games\` directories +- **EA Origin**: Games in `\Origin Games\` directories +- **Ubisoft Connect**: Games in `\Ubisoft Game Launcher\games\` directories -This file is not used in the current implementation since all routing and configuration are done within `Worker.cs`. +#### **Root Folder Detection** +Configure custom game directories for standalone games and non-platform installations: -- **Dependencies**: Uses `Microsoft.AspNetCore.Builder` and `Microsoft.AspNetCore.Hosting`. -- **Methods**: - - `ConfigureServices`: Placeholder method for adding services. - - `Configure`: Placeholder method for configuring application HTTP requests pipeline. +```json +"GameRootFolders": [ + "C:\\Games", + "D:\\Games", + "E:\\Games" +] +``` -### NvmlWrapper.cs +**How Root Folder Detection Works:** +- **Priority**: Root folders are checked **before** platform paths +- **Smart Naming**: Extracts game names from directory structure +- **Flexible Structure**: Supports any folder organization under root directories +- **Fallback Logic**: Uses file version info or executable name when needed -This file provides a C# wrapper for the NVIDIA Management Library (NVML) functions. +**Example Game Detection:** +``` +C:\Games\Cyberpunk 2077\bin\x64\Cyberpunk2077.exe + → Game Name: "Cyberpunk 2077" + → Platform: "Standalone" -- **Dependencies**: Uses `System` and `System.Runtime.InteropServices`. -- **Methods**: - - Importing NVML DLL functions to interact with GPU hardware. - - Structures like `NvmlUtilization` are defined for handling utilization rates returned by NVML. +D:\Games\The Witcher 3\witcher3.exe + → Game Name: "The Witcher 3" + → Platform: "Standalone" +``` -## Usage +**Configuration Tips:** +- Add drives where you install standalone games +- Include network drives if you store games on NAS +- Use absolute paths (e.g., `C:\Games`, not `Games`) +- Root folders are checked in order, so prioritize most common locations first -1. **Build the Project**: Use your preferred .NET build tool (e.g., `dotnet build`) to compile the project. -2. **Run the Application**: - - To run as a console application, execute the compiled binary directly. - - To run as a Windows service, use the command-line argument `--windows-service` or set the environment variable `RUN_AS_SERVICE` to `"true"`. +## 📊 Example API Responses -## APIs +### Health Check +```json +{ + "status": "Healthy", + "timestamp": "2025-08-07T02:30:00Z", + "uptime": "1.16:55:30", + "activeAlerts": 0, + "monitoringEnabled": { + "gpu": true, + "disk": true, + "network": true, + "temperature": true, + "processes": true, + "games": true, + "alerts": true + } +} +``` -- **Get Resource Usage**: - - URL: `/api/resource-usage` - - Method: GET - - Description: Retrieves current system resource usage. - -- **Kill Process**: - - URL: `/api/kill-process` - - Method: POST - - Body: JSON with the process ID (`{"id": "1234"}`) - - Description: Kills the specified process. +### CPU Usage +```json +{ + "usage": 15.5, + "coreUsages": [12.1, 18.3, 14.7, 16.2], + "temperature": 65.0, + "maxFrequency": 4400, + "currentFrequency": 3200, + "isThrottling": false +} +``` -## Important Notes +### VM Information +```json +{ + "isVirtualMachine": true, + "hypervisorVendor": "VMware", + "uptime": "1.16:55:30", + "bootTime": "2025-08-05T09:34:04Z", + "machineName": "WIN11-VM", + "domain": "WORKGROUP" +} +``` -- Ensure that the NVIDIA Management Library (NVML) is installed on the system for GPU monitoring to work. -- The application allows CORS from all origins, which should be configured securely in production environments. -- Error handling and logging are minimal; consider adding robust error handling and logging mechanisms for a production-ready solution. +### GPU Usage +```json +{ + "usage": 45, + "memoryUsage": 60, + "temperature": 72, + "fanSpeed": 65, + "powerUsage": 185000, + "memoryTotal": 8589934592, + "memoryUsed": 5153960755, + "isAvailable": true, + "name": "NVIDIA GeForce RTX 4070", + "driverVersion": "551.76", + "error": "" +} +``` -## Contributing +### Game Detection +```json +{ + "gameName": "Cyberpunk 2077", + "executableName": "Cyberpunk2077.exe", + "fullPath": "C:\\Games\\Cyberpunk 2077\\bin\\x64\\Cyberpunk2077.exe", + "processId": 8432, + "memoryUsage": 4294967296, + "cpuTime": "00:15:42.1250000", + "startTime": "2025-08-07T14:30:15.123456+08:00", + "platform": "Standalone", + "isFullscreen": true, + "fps": 0 +} +``` -Feel free to contribute by opening issues or submitting pull requests. Make sure to follow the project's coding style and best practices. +### Top Processes +```json +{ + "value": [ + { + "id": 11820, + "name": "WmiPrvSE", + "cpuUsage": 2.7276263, + "memoryUsage": 83120128, + "memoryUsagePercentage": 0.12576005, + "processorTime": "00:26:30.2500000", + "startTime": "2025-08-05T09:38:38.9837995+08:00", + "executablePath": "C:\\WINDOWS\\system32\\wbem\\wmiprvse.exe", + "commandLine": "C:\\WINDOWS\\system32\\wbem\\wmiprvse.exe" + }, + { + "id": 8376, + "name": "explorer", + "cpuUsage": 1.5750673, + "memoryUsage": 403636224, + "memoryUsagePercentage": 0.61069816, + "processorTime": "00:24:36.7968750", + "startTime": "2025-08-07T15:26:31.096813+08:00", + "executablePath": "C:\\WINDOWS\\Explorer.EXE", + "commandLine": "C:\\WINDOWS\\Explorer.EXE" + } + ], + "count": 2 +} +``` +## 🔧 PowerShell Usage Examples -# devnote -dotnet run -git add . -git commit -m "Add steam running games" -git push origin master -dotnet publish -c Release -o ./publish +```powershell +# Get system health +$health = Invoke-RestMethod -Uri "http://localhost:5000/api/health" +Write-Host "System Status: $($health.status)" -# devtest -Invoke-WebRequest -Uri "http://localhost:5000/api/kill-process" -Method POST -Body "1234" -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } +# Get CPU usage +$cpu = Invoke-RestMethod -Uri "http://localhost:5000/api/cpu-usage" +Write-Host "CPU Usage: $($cpu.usage)%" -Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/resource-usage" -Method GET -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } +# Get GPU usage +$gpu = Invoke-RestMethod -Uri "http://localhost:5000/api/gpu-usage" +if ($gpu.isAvailable) { + Write-Host "GPU: $($gpu.name)" + Write-Host "GPU Usage: $($gpu.usage)%" + Write-Host "GPU Memory: $([math]::Round($gpu.memoryUsed / 1GB, 2))GB / $([math]::Round($gpu.memoryTotal / 1GB, 2))GB ($($gpu.memoryUsage)%)" + Write-Host "GPU Temperature: $($gpu.temperature)°C" +} else { + Write-Host "GPU not available: $($gpu.error)" +} -Use 'shutdown', 'restart', or 'cancel'. -Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/force-shutdown" ` --Method POST ` --Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } ` --Body '{"Action": "shutdown", "DelaySeconds": 120}' ` --ContentType "application/json" +# Get current game (enhanced with root folder detection) +$game = Invoke-RestMethod -Uri "http://localhost:5000/api/current-game" +if ($game) { + Write-Host "Currently playing: $($game.gameName)" + Write-Host "Platform: $($game.platform)" + Write-Host "Executable: $($game.executableName)" + if ($game.isFullscreen) { + Write-Host "Running in fullscreen mode" + } +} else { + Write-Host "No game currently detected" +} + +# Get all detected games +$allGames = Invoke-RestMethod -Uri "http://localhost:5000/api/all-games" +Write-Host "Detected games on system:" +foreach ($gameItem in $allGames) { + Write-Host " $($gameItem.gameName) ($($gameItem.platform)) - Memory: $([math]::Round($gameItem.memoryUsage / 1MB, 0))MB" +} + +# Get top processes by CPU usage +$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/top-processes?count=5" +Write-Host "Top 5 processes by CPU usage:" +foreach ($proc in $processes.value) { + Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F2'))% CPU, $($proc.memoryUsagePercentage.ToString('F2'))% Memory" +} + +# Terminate a process (example - be careful!) +$killRequest = @{ + ProcessId = 1234 + Force = $false +} | ConvertTo-Json + +# Invoke-RestMethod -Uri "http://localhost:5000/api/process/kill" -Method Post -Body $killRequest -ContentType "application/json" + +# Shutdown system with 60-second delay +$shutdownRequest = @{ + Action = "shutdown" + DelaySeconds = 60 + Message = "Scheduled maintenance shutdown" +} | ConvertTo-Json + +Invoke-RestMethod -Uri "http://localhost:5000/api/system/shutdown" -Method Post -Body $shutdownRequest -ContentType "application/json" +``` + +## 🚨 Known Warnings (Non-Critical) + +The service may show warnings in VM environments that don't affect functionality: + +- **Performance Counter Warnings**: Some performance counters may not be available in VMs +- **Temperature Sensor Access**: Some temperature sensors require elevated privileges +- **Process Access Denied**: Some system processes require elevated privileges to access +- **Windows.Forms Compatibility**: Game detection works despite .NET Framework compatibility warnings + +These warnings are expected in VM environments and the service continues to function normally. + +## 🎯 Perfect for Unraid + +This service is specifically optimized for Windows VMs running on Unraid: + +- **VM Detection**: Automatically detects and reports virtualization status +- **Resource Monitoring**: Tracks VM resource allocation and usage +- **Gaming Support**: Detects games and monitors performance impact +- **Remote Management**: Full API control for integration with Unraid dashboard +- **Alert System**: Configurable alerts for resource thresholds +- **Health Monitoring**: Comprehensive health checks for VM status + +## 📝 Logging + +The service uses Serilog for structured logging: +- Console output for real-time monitoring +- File logging for persistent records +- Configurable log levels (Debug, Information, Warning, Error) +- Smart error suppression to prevent log spam in VM environments + +## 🔐 Security + +- Optional API key authentication +- CORS support for web dashboard integration +- Process termination requires explicit API calls +- System shutdown/restart requires explicit API calls +- Configurable allowed origins for API access + +## 📈 Performance + +- Lightweight background monitoring (5-second intervals by default) +- Efficient memory usage with smart caching and cleanup of old process data +- Non-blocking async operations +- Real-time CPU usage calculation for individual processes +- Graceful error handling for VM-specific limitations +- Configurable monitoring intervals and features +- Smart process tracking with automatic cleanup to prevent memory leaks + +## 🆘 Support + +For issues or questions: +1. Check the console output for warnings/errors +2. Review the configuration in `appsettings.json` +3. Test individual API endpoints using PowerShell or curl +4. Check Windows Event Logs if running as a service + +--- + +**Version**: 2.1.0 +**Target Framework**: .NET 9.0 +**Platforms**: Windows (VM optimized) +**License**: Open Source + +### Recent Updates +- **v2.1.0**: Added configurable game root folders for enhanced standalone game detection +- **v2.0.0**: Initial release with comprehensive system monitoring and game detection diff --git a/README_v2.md b/README_v2.md deleted file mode 100644 index e046117..0000000 --- a/README_v2.md +++ /dev/null @@ -1,248 +0,0 @@ -# Resource Monitor Service for Unraid VM - -A comprehensive system monitoring service specifically designed for Windows VMs running on Unraid servers. This service provides real-time monitoring of CPU, memory, GPU, disk, network, and system resources through a RESTful API. - -## 🚀 Features - -### Core Monitoring -- **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection -- **Memory Monitoring**: RAM usage, available memory, committed memory, and paging -- **GPU Monitoring**: NVIDIA GPU usage, memory, temperature (via NVML) -- **Disk Monitoring**: I/O statistics, space usage, and performance counters -- **Network Monitoring**: Bandwidth usage, packet statistics, and interface data -- **Temperature Monitoring**: CPU and hard drive temperature sensors - -### VM-Specific Features -- **VM Detection**: Automatically detects virtualization environment -- **Hypervisor Identification**: Identifies VMware, VirtualBox, Hyper-V, KVM, etc. -- **Unraid Optimization**: Optimized for Unraid VM environments -- **Resource Alerting**: Configurable thresholds for resource usage alerts - -### Advanced Features -- **Game Detection**: Multi-platform game detection with fullscreen monitoring -- **Process Management**: View top processes, terminate processes via API -- **Smart Alerting**: Duration-based alerting to prevent false positives -- **System Control**: Remote shutdown/restart capabilities -- **Health Monitoring**: Comprehensive health checks and uptime tracking - -## 📡 API Endpoints - -The service runs on `http://localhost:5000` by default and provides the following endpoints: - -### System Information -- `GET /api/system-info` - Complete system information including VM details -- `GET /api/vm/info` - VM-specific information (hypervisor, uptime, etc.) -- `GET /api/health` - Service health status and monitoring capabilities -- `GET /api/metrics` - Service metrics and performance overview -- `GET /api/config` - Current configuration settings - -### Resource Monitoring -- `GET /api/resource-usage` - Complete resource usage overview -- `GET /api/cpu-usage` - Detailed CPU metrics with per-core data -- `GET /api/memory-usage` - Memory utilization and statistics -- `GET /api/gpu-usage` - GPU usage, memory, and temperature -- `GET /api/disk-usage` - Disk I/O and space usage for all drives -- `GET /api/network-usage` - Network interface statistics -- `GET /api/top-processes?count=10` - Top processes by CPU/memory usage - -### Game Detection -- `GET /api/current-game` - Currently running game information -- `GET /api/all-games` - All detected games on the system -- `GET /api/fullscreen-status` - Check if any game is running fullscreen - -### Alerting System -- `GET /api/alerts/active` - Currently active alerts -- `GET /api/alerts/history?count=100` - Alert history -- `POST /api/alerts/{alertId}/resolve` - Manually resolve an alert -- `GET /api/alerts/enabled` - Check if alerting is enabled - -### System Control -- `POST /api/process/kill` - Terminate a process (requires process ID and optional force flag) -- `POST /api/system/shutdown` - Shutdown, restart, or cancel system operations -- `POST /api/service/stop` - Stop the monitoring service - -## 🛠️ Installation & Usage - -### Option 1: Console Application (Development/Testing) -```powershell -cd C:\Work\DEV\ResourceUsageAPI -dotnet run --configuration Release -``` - -### Option 2: Windows Service (Production) -```powershell -# Run as Administrator -cd C:\Work\DEV\ResourceUsageAPI\publish -.\install-service.bat -``` - -### Option 3: Standalone Executable -```powershell -cd C:\Work\DEV\ResourceUsageAPI\publish -.\ResourceMonitorService.exe -``` - -## ⚙️ Configuration - -Configuration is managed through `appsettings.json`: - -```json -{ - "MonitoringSettings": { - "UpdateIntervalMs": 5000, - "EnableGpuMonitoring": true, - "EnableDiskMonitoring": true, - "EnableNetworkMonitoring": true, - "EnableTemperatureMonitoring": true, - "EnableProcessMonitoring": true, - "EnableGameDetection": true, - "EnableAlerts": true - }, - "AlertThresholds": { - "CpuUsageThreshold": 80.0, - "MemoryUsageThreshold": 85.0, - "GpuUsageThreshold": 90.0, - "DiskUsageThreshold": 90.0, - "TemperatureThreshold": 80.0, - "AlertDurationSeconds": 30 - }, - "ApiSettings": { - "RequireApiKey": false, - "AllowedOrigins": ["http://localhost:4200", "http://unraid:4200"], - "BasePath": "/api" - } -} -``` - -## 📊 Example API Responses - -### Health Check -```json -{ - "status": "Healthy", - "timestamp": "2025-08-07T02:30:00Z", - "uptime": "1.16:55:30", - "activeAlerts": 0, - "monitoringEnabled": { - "gpu": true, - "disk": true, - "network": true, - "temperature": true, - "processes": true, - "games": true, - "alerts": true - } -} -``` - -### CPU Usage -```json -{ - "usage": 15.5, - "coreUsages": [12.1, 18.3, 14.7, 16.2], - "temperature": 65.0, - "maxFrequency": 4400, - "currentFrequency": 3200, - "isThrottling": false -} -``` - -### VM Information -```json -{ - "isVirtualMachine": true, - "hypervisorVendor": "VMware", - "uptime": "1.16:55:30", - "bootTime": "2025-08-05T09:34:04Z", - "machineName": "WIN11-VM", - "domain": "WORKGROUP" -} -``` - -## 🔧 PowerShell Usage Examples - -```powershell -# Get system health -$health = Invoke-RestMethod -Uri "http://localhost:5000/api/health" -Write-Host "System Status: $($health.status)" - -# Get CPU usage -$cpu = Invoke-RestMethod -Uri "http://localhost:5000/api/cpu-usage" -Write-Host "CPU Usage: $($cpu.usage)%" - -# Get current game -$game = Invoke-RestMethod -Uri "http://localhost:5000/api/current-game" -if ($game.isGameRunning) { - Write-Host "Currently playing: $($game.gameName)" -} - -# Shutdown system with 60-second delay -$shutdownRequest = @{ - Action = "shutdown" - DelaySeconds = 60 - Message = "Scheduled maintenance shutdown" -} | ConvertTo-Json - -Invoke-RestMethod -Uri "http://localhost:5000/api/system/shutdown" -Method Post -Body $shutdownRequest -ContentType "application/json" -``` - -## 🚨 Known Warnings (Non-Critical) - -The service may show warnings in VM environments that don't affect functionality: - -- **Performance Counter Warnings**: Some performance counters may not be available in VMs -- **Temperature Sensor Access**: Some temperature sensors require elevated privileges -- **Process Access Denied**: Some system processes require elevated privileges to access -- **Windows.Forms Compatibility**: Game detection works despite .NET Framework compatibility warnings - -These warnings are expected in VM environments and the service continues to function normally. - -## 🎯 Perfect for Unraid - -This service is specifically optimized for Windows VMs running on Unraid: - -- **VM Detection**: Automatically detects and reports virtualization status -- **Resource Monitoring**: Tracks VM resource allocation and usage -- **Gaming Support**: Detects games and monitors performance impact -- **Remote Management**: Full API control for integration with Unraid dashboard -- **Alert System**: Configurable alerts for resource thresholds -- **Health Monitoring**: Comprehensive health checks for VM status - -## 📝 Logging - -The service uses Serilog for structured logging: -- Console output for real-time monitoring -- File logging for persistent records -- Configurable log levels (Debug, Information, Warning, Error) -- Smart error suppression to prevent log spam in VM environments - -## 🔐 Security - -- Optional API key authentication -- CORS support for web dashboard integration -- Process termination requires explicit API calls -- System shutdown/restart requires explicit API calls -- Configurable allowed origins for API access - -## 📈 Performance - -- Lightweight background monitoring (5-second intervals by default) -- Efficient memory usage with smart caching -- Non-blocking async operations -- Graceful error handling for VM-specific limitations -- Configurable monitoring intervals and features - -## 🆘 Support - -For issues or questions: -1. Check the console output for warnings/errors -2. Review the configuration in `appsettings.json` -3. Test individual API endpoints using PowerShell or curl -4. Check Windows Event Logs if running as a service - ---- - -**Version**: 2.0.0 -**Target Framework**: .NET 9.0 -**Platforms**: Windows (VM optimized) -**License**: Open Source diff --git a/ResourceMonitorService.csproj b/ResourceMonitorService.csproj index 63f0c4b..43cc2ed 100644 --- a/ResourceMonitorService.csproj +++ b/ResourceMonitorService.csproj @@ -1,10 +1,11 @@ - net9.0 + net9.0-windows enable enable dotnet-ResourceMonitorService-ff17df27-9a94-433d-84e9-744dd4b626c2 + true @@ -13,7 +14,6 @@ - diff --git a/Services/GameDetectionService.cs b/Services/GameDetectionService.cs index 718add7..2e60e9e 100644 --- a/Services/GameDetectionService.cs +++ b/Services/GameDetectionService.cs @@ -201,6 +201,13 @@ namespace ResourceMonitorService.Services { try { + // First check configured game root folders + var gameFromRootFolder = DetectGameFromRootFolders(filePath, process); + if (gameFromRootFolder != null) + { + return gameFromRootFolder; + } + // Check each configured game platform path foreach (var platformPath in _settings.GamePlatformPaths) { @@ -227,10 +234,28 @@ namespace ResourceMonitorService.Services // Additional checks for common game launchers and executables var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant(); + + // Exclude known system processes and applications + var systemExclusions = new[] + { + "officeclicktorun", "winword", "excel", "powerpoint", "outlook", + "teams", "skype", "chrome", "firefox", "edge", "explorer", + "notepad", "calculator", "cmd", "powershell", "taskmgr", + "svchost", "dwm", "csrss", "winlogon", "lsass", "services", + "wininit", "audiodg", "conhost", "rundll32", "msiexec", + "setup", "installer", "update", "vshost", "devenv" + }; + + // Skip if it's a known system process + if (systemExclusions.Any(exclusion => fileName.Contains(exclusion))) + { + return null; + } + var knownGameExecutables = new[] { - "game", "launcher", "client", "main", "start", "run", - // Add more common game executable patterns + "game", "launcher", "client" + // Removed generic terms like "main", "start", "run" that match too many system processes }; var gameIndicators = new[] @@ -240,8 +265,10 @@ namespace ResourceMonitorService.Services }; // 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))) + // Made the condition more restrictive to reduce false positives + if ((knownGameExecutables.Any(exe => fileName.Equals(exe) || fileName.StartsWith(exe + ".")) || + gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase))) && + !filePath.Contains("Program Files\\Common Files", StringComparison.OrdinalIgnoreCase)) { // Try to determine platform and game name from other indicators var platform = DeterminePlatformFromProcess(process, filePath); @@ -394,6 +421,84 @@ namespace ResourceMonitorService.Services } } + private GameInfo? DetectGameFromRootFolders(string filePath, Process process) + { + try + { + foreach (var rootFolder in _settings.GameRootFolders) + { + if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase)) + { + var gameName = ExtractGameNameFromRootFolder(filePath, rootFolder); + + 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 = "Standalone", // Games in root folders are typically standalone + IsFullscreen = false, // Will be set by caller + FPS = 0f // Will be set by caller + }; + } + } + + return null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error detecting game from root folders for path {FilePath}", filePath); + return null; + } + } + + private string ExtractGameNameFromRootFolder(string filePath, string rootFolder) + { + try + { + // Remove the root folder from the path to get the relative game path + var relativePath = filePath.Substring(rootFolder.Length).TrimStart('\\', '/'); + + // Split by directory separator and take the first part as the game folder + var pathParts = relativePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, + StringSplitOptions.RemoveEmptyEntries); + + if (pathParts.Length > 0) + { + var gameFolder = pathParts[0]; + + // If the game folder name is reasonable, use it + if (!string.IsNullOrEmpty(gameFolder) && + !gameFolder.Equals("bin", StringComparison.OrdinalIgnoreCase) && + !gameFolder.Equals("exe", StringComparison.OrdinalIgnoreCase) && + !gameFolder.Equals("data", StringComparison.OrdinalIgnoreCase)) + { + return gameFolder; + } + } + + // Fallback: try to get the game name from file properties + var versionInfo = FileVersionInfo.GetVersionInfo(filePath); + if (!string.IsNullOrEmpty(versionInfo.ProductName) && + !versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase)) + { + return versionInfo.ProductName; + } + + // Last resort: use the executable name + return Path.GetFileNameWithoutExtension(filePath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not extract game name from root folder path {FilePath}", filePath); + return Path.GetFileNameWithoutExtension(filePath); + } + } + private int GetParentProcessId(int processId) { try diff --git a/Services/ResourceMonitorService.cs b/Services/ResourceMonitorService.cs index 053c7b1..3ac0dfe 100644 --- a/Services/ResourceMonitorService.cs +++ b/Services/ResourceMonitorService.cs @@ -25,6 +25,10 @@ namespace ResourceMonitorService.Services private readonly Dictionary _counters; private readonly Dictionary _previousNetworkBytes; private readonly Dictionary _previousNetworkTime; + private readonly Dictionary _previousDiskBytes; + private readonly Dictionary _previousDiskTime; + private readonly Dictionary _errorCounts; + private readonly Dictionary _previousProcessorTimes; public ResourceMonitorService(ILogger logger, IOptions settings) { @@ -33,6 +37,10 @@ namespace ResourceMonitorService.Services _counters = new Dictionary(); _previousNetworkBytes = new Dictionary(); _previousNetworkTime = new Dictionary(); + _previousDiskBytes = new Dictionary(); + _previousDiskTime = new Dictionary(); + _errorCounts = new Dictionary(); + _previousProcessorTimes = new Dictionary(); InitializeCounters(); } @@ -59,6 +67,7 @@ namespace ResourceMonitorService.Services #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 @@ -67,9 +76,10 @@ namespace ResourceMonitorService.Services } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to initialize performance counter: {CounterName}", counter.CounterName); + LogSuppressedWarning("counter_init", ex, $"Failed to initialize performance counter: {counter.CounterName}"); } } +#pragma warning restore CA1416 // Validate platform compatibility } catch (Exception ex) { @@ -77,6 +87,22 @@ namespace ResourceMonitorService.Services } } + 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 GetResourceUsageAsync() { var timestamp = DateTime.Now; @@ -274,27 +300,46 @@ namespace ResourceMonitorService.Services uint temperature; NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature); - uint fanSpeed; - NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed); + 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"); + } - // Try to get additional information + 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"; - var memoryTotal = 0UL; - var memoryUsed = 0UL; - var powerUsage = 0U; try { - // Get GPU name and memory info via WMI as fallback + // 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) { - name = obj["Name"]?.ToString() ?? name; driverVersion = obj["DriverVersion"]?.ToString() ?? driverVersion; - if (obj["AdapterRAM"] != null) + // If NVML memory call failed, try WMI as fallback + if (memoryTotal == 0 && obj["AdapterRAM"] != null) { memoryTotal = (ulong)obj["AdapterRAM"]; } @@ -304,7 +349,7 @@ namespace ResourceMonitorService.Services } catch (Exception ex) { - _logger.LogWarning(ex, "Could not get additional GPU information via WMI"); + LogSuppressedWarning("gpu_wmi", ex, "Could not get additional GPU information via WMI"); } NvmlWrapper.NvmlShutdown(); @@ -315,7 +360,7 @@ namespace ResourceMonitorService.Services MemoryUsage = utilization.Memory, Temperature = temperature, FanSpeed = fanSpeed, - PowerUsage = powerUsage, + PowerUsage = powerUsage, // Power in milliwatts MemoryTotal = memoryTotal, MemoryUsed = memoryUsed, IsAvailable = true, @@ -357,6 +402,7 @@ namespace ResourceMonitorService.Services return await Task.Run(() => { var diskUsages = new List(); + var timestamp = DateTime.Now; var drives = DriveInfo.GetDrives(); foreach (var drive in drives) @@ -374,50 +420,14 @@ namespace ResourceMonitorService.Services 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); + // Get disk performance data with proper timing + GetDiskPerformanceData(drive, diskUsage, timestamp); - readCounter.NextValue(); - writeCounter.NextValue(); - timeCounter.NextValue(); - - Thread.Sleep(1000); + // Get disk temperature using SMART data + GetDiskTemperature(drive, diskUsage); - 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); - } + // Get additional disk information + GetDiskInfo(drive, diskUsage); diskUsages.Add(diskUsage); } @@ -433,6 +443,217 @@ namespace ResourceMonitorService.Services } } + 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 GetNetworkUsageAsync() { try @@ -524,35 +745,89 @@ namespace ResourceMonitorService.Services { return await Task.Run(() => { - var processes = Process.GetProcesses() - .Where(p => !p.HasExited) - .Select(p => + var allProcesses = Process.GetProcesses(); + _logger.LogDebug("Found {ProcessCount} total processes", allProcesses.Length); + + var validProcesses = new List(); + 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 + { + Id = 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 { - 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) - }; + processInfo.StartTime = p.StartTime; } catch { - return null; // Skip processes that throw exceptions + processInfo.StartTime = DateTime.MinValue; } - }) - .Where(p => p != null) - .OrderByDescending(p => p!.MemoryUsage) + + 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.Id).ToHashSet()); + + var topProcesses = validProcesses + .OrderByDescending(p => p.CpuUsage) + .ThenByDescending(p => p.MemoryUsage) .Take(count) - .Cast() .ToList(); - return processes; + _logger.LogDebug("Returning {TopCount} top processes", topProcesses.Count); + return topProcesses; }); } catch (Exception ex) @@ -583,7 +858,7 @@ namespace ResourceMonitorService.Services } catch (Exception ex) { - _logger.LogWarning(ex, "Could not get CPU temperature"); + LogSuppressedWarning("cpu_temperature", ex, "Could not get CPU temperature"); return 0f; } } @@ -600,28 +875,95 @@ namespace ResourceMonitorService.Services HardDrives = new List() }; - // Try to get hard drive temperatures + // Get hard drive temperatures using improved method 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 drives = DriveInfo.GetDrives(); + foreach (var drive in drives.Where(d => d.IsReady && d.DriveType == DriveType.Fixed)) { - var instanceName = obj["InstanceName"]?.ToString() ?? ""; - // This would need more sophisticated parsing for actual SMART data - temperatureInfo.HardDrives.Add(new HardDriveTemp + var hardDriveTemp = new HardDriveTemp { - Drive = instanceName, - Temperature = 0f, // Would need SMART data parsing + 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) { - _logger.LogWarning(ex, "Could not get hard drive temperatures"); + LogSuppressedWarning("hdd_temperature", ex, "Could not get hard drive temperatures"); } return temperatureInfo; @@ -654,6 +996,104 @@ namespace ResourceMonitorService.Services } } + 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 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 diff --git a/WorkerNew.cs b/WorkerNew.cs deleted file mode 100644 index 6a93c03..0000000 --- a/WorkerNew.cs +++ /dev/null @@ -1,387 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using ResourceMonitorService.Configuration; -using ResourceMonitorService.Models; -using ResourceMonitorService.Services; -using System.Diagnostics; - -namespace ResourceMonitorService -{ - public class Worker : BackgroundService - { - private readonly ILogger _logger; - private readonly IHostApplicationLifetime _lifetime; - private readonly ISystemInfoService _systemInfoService; - private readonly IResourceMonitorService _resourceMonitorService; - private readonly IGameDetectionService _gameDetectionService; - private readonly IAlertService _alertService; - private readonly ApiSettings _apiSettings; - private readonly MonitoringSettings _monitoringSettings; - - public Worker( - ILogger logger, - IHostApplicationLifetime lifetime, - ISystemInfoService systemInfoService, - IResourceMonitorService resourceMonitorService, - IGameDetectionService gameDetectionService, - IAlertService alertService, - IOptions apiSettings, - IOptions monitoringSettings) - { - _logger = logger; - _lifetime = lifetime; - _systemInfoService = systemInfoService; - _resourceMonitorService = resourceMonitorService; - _gameDetectionService = gameDetectionService; - _alertService = alertService; - _apiSettings = apiSettings.Value; - _monitoringSettings = monitoringSettings.Value; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Resource Monitor Service starting..."); - - var builder = WebApplication.CreateBuilder(); - - // Configure CORS - builder.Services.AddCors(options => - { - options.AddPolicy("AllowedOrigins", policy => - policy.WithOrigins(_apiSettings.AllowedOrigins.ToArray()) - .AllowAnyHeader() - .AllowAnyMethod()); - }); - - builder.Services.AddControllers().AddNewtonsoftJson(); - - var app = builder.Build(); - - // API Key middleware (if enabled) - if (_apiSettings.RequireApiKey) - { - app.Use(async (context, next) => - { - if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) || - extractedApiKey != _apiSettings.ApiKey) - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - await context.Response.WriteAsync("Unauthorized: Invalid API Key"); - return; - } - await next(); - }); - } - - app.UseCors("AllowedOrigins"); - - // Enhanced API endpoints - ConfigureApiEndpoints(app); - - // Start background monitoring - _ = Task.Run(async () => await BackgroundMonitoringLoop(stoppingToken), stoppingToken); - - // Start the web application - _ = app.RunAsync(stoppingToken); - - await Task.Delay(Timeout.Infinite, stoppingToken); - } - - private void ConfigureApiEndpoints(WebApplication app) - { - var basePath = _apiSettings.BasePath; - - // System information endpoints - app.MapGet($"{basePath}/system-info", async () => - Results.Ok(await _systemInfoService.GetSystemInfoAsync())); - - // Resource usage endpoints - app.MapGet($"{basePath}/resource-usage", async () => - Results.Ok(await _resourceMonitorService.GetResourceUsageAsync())); - - app.MapGet($"{basePath}/cpu-usage", async () => - Results.Ok(await _resourceMonitorService.GetCpuUsageAsync())); - - app.MapGet($"{basePath}/memory-usage", async () => - Results.Ok(await _resourceMonitorService.GetMemoryUsageAsync())); - - app.MapGet($"{basePath}/gpu-usage", async () => - Results.Ok(await _resourceMonitorService.GetGpuUsageAsync())); - - app.MapGet($"{basePath}/disk-usage", async () => - Results.Ok(await _resourceMonitorService.GetDiskUsageAsync())); - - app.MapGet($"{basePath}/network-usage", async () => - Results.Ok(await _resourceMonitorService.GetNetworkUsageAsync())); - - app.MapGet($"{basePath}/top-processes", async (int count = 10) => - Results.Ok(await _resourceMonitorService.GetTopProcessesAsync(count))); - - // Game detection endpoints - app.MapGet($"{basePath}/current-game", async () => - Results.Ok(await _gameDetectionService.GetCurrentlyRunningGameAsync())); - - app.MapGet($"{basePath}/all-games", async () => - Results.Ok(await _gameDetectionService.GetAllDetectedGamesAsync())); - - app.MapGet($"{basePath}/fullscreen-status", async () => - Results.Ok(new { IsFullscreen = await _gameDetectionService.IsGameRunningFullscreenAsync() })); - - // Alert endpoints - app.MapGet($"{basePath}/alerts/active", async () => - Results.Ok(await _alertService.GetActiveAlertsAsync())); - - app.MapGet($"{basePath}/alerts/history", async (int count = 100) => - Results.Ok(await _alertService.GetAlertHistoryAsync(count))); - - app.MapPost($"{basePath}/alerts/{{alertId}}/resolve", async (string alertId) => - { - await _alertService.ResolveAlertAsync(alertId); - return Results.Ok(new { Message = "Alert resolved successfully" }); - }); - - app.MapGet($"{basePath}/alerts/enabled", async () => - Results.Ok(new { Enabled = await _alertService.IsAlertingEnabledAsync() })); - - // Process management endpoints (enhanced) - app.MapPost($"{basePath}/process/kill", async (HttpContext context) => - { - try - { - var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); - var request = JsonConvert.DeserializeObject(body); - - int processId = request?.ProcessId ?? 0; - bool force = request?.Force ?? false; - - if (processId <= 0) - { - return Results.BadRequest("Invalid process ID"); - } - - var processes = Process.GetProcesses().Where(p => p.Id == processId).ToArray(); - - if (processes.Length == 0) - { - return Results.NotFound($"No process found with ID {processId}"); - } - - var process = processes[0]; - var processName = process.ProcessName; - - if (force) - { - process.Kill(true); // Force kill entire process tree - } - else - { - process.CloseMainWindow(); // Try graceful close first - - // Wait a bit for graceful close - await Task.Delay(3000); - - if (!process.HasExited) - { - process.Kill(); - } - } - - _logger.LogWarning("Process {ProcessName} (ID: {ProcessId}) was terminated", processName, processId); - return Results.Ok(new { Message = $"Process {processName} (ID: {processId}) has been terminated." }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error terminating process"); - return Results.Problem(ex.Message); - } - }); - - // Enhanced shutdown/restart endpoints - app.MapPost($"{basePath}/system/shutdown", async (HttpContext context) => - { - try - { - var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); - var request = JsonConvert.DeserializeObject(body); - - string action = request?.Action?.ToString()?.ToLower() ?? "shutdown"; - int delaySeconds = request?.DelaySeconds ?? 0; - string message = request?.Message?.ToString() ?? "System shutdown initiated by Resource Monitor"; - - if (action != "shutdown" && action != "restart" && action != "cancel") - { - return Results.BadRequest("Invalid action. Use 'shutdown', 'restart', or 'cancel'."); - } - - if (action == "cancel") - { - var cancelProcess = new ProcessStartInfo - { - FileName = "shutdown", - Arguments = "/a", - CreateNoWindow = true, - UseShellExecute = false - }; - Process.Start(cancelProcess); - - _logger.LogWarning("System shutdown cancelled"); - return Results.Ok(new { Message = "Shutdown cancelled." }); - } - - if (delaySeconds < 0) - { - return Results.BadRequest("Delay must be a non-negative integer."); - } - - string shutdownCommand = action == "shutdown" - ? $"/s /f /t {delaySeconds} /c \"{message}\"" - : $"/r /f /t {delaySeconds} /c \"{message}\""; - - var processStartInfo = new ProcessStartInfo - { - FileName = "shutdown", - Arguments = shutdownCommand, - CreateNoWindow = true, - UseShellExecute = false - }; - - Process.Start(processStartInfo); - - _logger.LogWarning("System {Action} initiated with {Delay} seconds delay", action, delaySeconds); - return Results.Ok(new { - Message = $"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds.", - Action = action, - DelaySeconds = delaySeconds, - Timestamp = DateTime.Now - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing system command"); - return Results.Problem(ex.Message); - } - }); - - // VM-specific endpoints for Unraid - app.MapGet($"{basePath}/vm/info", async () => - { - var systemInfo = await _systemInfoService.GetSystemInfoAsync(); - return Results.Ok(new - { - IsVirtualMachine = systemInfo.IsVirtualMachine, - HypervisorVendor = systemInfo.HypervisorVendor, - Uptime = systemInfo.Uptime, - BootTime = systemInfo.BootTime, - MachineName = systemInfo.MachineName, - Domain = systemInfo.Domain - }); - }); - - // Performance history endpoint (simple in-memory storage) - app.MapGet($"{basePath}/performance/history", async (int minutes = 60) => - { - // This would ideally be stored in a database or time-series database - // For now, return current snapshot with timestamp - var usage = await _resourceMonitorService.GetResourceUsageAsync(); - return Results.Ok(new { - Current = usage, - Message = "Historical data not implemented yet - showing current values" - }); - }); - - // Health check endpoint - app.MapGet($"{basePath}/health", async () => - { - var systemInfo = await _systemInfoService.GetSystemInfoAsync(); - var alerts = await _alertService.GetActiveAlertsAsync(); - - return Results.Ok(new - { - Status = "Healthy", - Timestamp = DateTime.Now, - Uptime = systemInfo.Uptime, - ActiveAlerts = alerts.Count, - MonitoringEnabled = new - { - GPU = _monitoringSettings.EnableGpuMonitoring, - Disk = _monitoringSettings.EnableDiskMonitoring, - Network = _monitoringSettings.EnableNetworkMonitoring, - Temperature = _monitoringSettings.EnableTemperatureMonitoring, - Processes = _monitoringSettings.EnableProcessMonitoring, - Games = _monitoringSettings.EnableGameDetection, - Alerts = _monitoringSettings.EnableAlerts - } - }); - }); - - // Service control endpoints - app.MapPost($"{basePath}/service/stop", () => - { - _logger.LogWarning("Service stop requested via API"); - _lifetime.StopApplication(); - return Results.Ok(new { Message = "Stopping the service..." }); - }); - - // Root endpoint - app.MapGet("/", () => Results.Ok(new - { - Service = "Resource Monitor Service", - Version = "2.0.0", - Status = "Running", - Timestamp = DateTime.Now, - ApiBasePath = _apiSettings.BasePath, - Documentation = $"{_apiSettings.BasePath}/health" - })); - - _logger.LogInformation("API endpoints configured. Base path: {BasePath}", _apiSettings.BasePath); - } - - private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken) - { - _logger.LogInformation("Background monitoring started"); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - // Get current resource usage - var resourceUsage = await _resourceMonitorService.GetResourceUsageAsync(); - - // Add current game info if game detection is enabled - if (_monitoringSettings.EnableGameDetection) - { - resourceUsage.RunningGame = await _gameDetectionService.GetCurrentlyRunningGameAsync(); - } - - // Check for alerts - if (_monitoringSettings.EnableAlerts) - { - await _alertService.CheckAndGenerateAlertsAsync(resourceUsage); - } - - // Log performance metrics occasionally - if (DateTime.Now.Second % 30 == 0) // Every 30 seconds - { - _logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%", - resourceUsage.CPU.Usage, - resourceUsage.Memory.UsagePercentage, - resourceUsage.GPU.Usage); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in background monitoring loop"); - } - - await Task.Delay(_monitoringSettings.UpdateIntervalMs, cancellationToken); - } - - _logger.LogInformation("Background monitoring stopped"); - } - } -} diff --git a/appsettings.json b/appsettings.json index a6c7da4..e70c932 100644 --- a/appsettings.json +++ b/appsettings.json @@ -2,7 +2,10 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "ResourceMonitorService.Services.ResourceMonitorService": "Error", + "ResourceMonitorService.Services.GameDetectionService": "Error", + "System": "Warning" } }, "RunAsWindowsService": true, @@ -44,6 +47,11 @@ "\\Origin Games\\", "\\Ubisoft Game Launcher\\games\\" ], + "GameRootFolders": [ + "C:\\Games", + "D:\\Games", + "E:\\Games" + ], "AlertThresholds": [ { "Component": "CPU",