feature/ai-resource-monitor #3
@@ -1,6 +1,10 @@
|
|||||||
# Ignore all .log files
|
# Ignore all .log files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Ignore logs directory and log files
|
||||||
|
logs/
|
||||||
|
*.txt
|
||||||
|
|
||||||
# Ignore all .tmp files
|
# Ignore all .tmp files
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ namespace ResourceMonitorService.Configuration
|
|||||||
@"\Ubisoft Game Launcher\games\"
|
@"\Ubisoft Game Launcher\games\"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public List<string> GameRootFolders { get; set; } = new()
|
||||||
|
{
|
||||||
|
@"C:\Games",
|
||||||
|
@"D:\Games",
|
||||||
|
@"E:\Games"
|
||||||
|
};
|
||||||
|
|
||||||
public List<AlertThresholdConfig> AlertThresholds { get; set; } = new()
|
public List<AlertThresholdConfig> AlertThresholds { get; set; } = new()
|
||||||
{
|
{
|
||||||
new() { Component = "CPU", WarningThreshold = 80, CriticalThreshold = 95, DurationSeconds = 30 },
|
new() { Component = "CPU", WarningThreshold = 80, CriticalThreshold = 95, DurationSeconds = 30 },
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ namespace ResourceMonitorService.Models
|
|||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public float CpuUsage { get; set; }
|
public float CpuUsage { get; set; }
|
||||||
public ulong MemoryUsage { get; set; }
|
public ulong MemoryUsage { get; set; }
|
||||||
|
public float MemoryUsagePercentage { get; set; }
|
||||||
public TimeSpan ProcessorTime { get; set; }
|
public TimeSpan ProcessorTime { get; set; }
|
||||||
public DateTime StartTime { get; set; }
|
public DateTime StartTime { get; set; }
|
||||||
public string ExecutablePath { get; set; } = string.Empty;
|
public string ExecutablePath { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -30,10 +30,27 @@ public static class NvmlWrapper
|
|||||||
[DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetFanSpeed")]
|
[DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetFanSpeed")]
|
||||||
public static extern int NvmlDeviceGetFanSpeed(IntPtr device, out uint speed);
|
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)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public struct NvmlUtilization
|
public struct NvmlUtilization
|
||||||
{
|
{
|
||||||
public uint Gpu;
|
public uint Gpu;
|
||||||
public uint Memory;
|
public uint Memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct NvmlMemory
|
||||||
|
{
|
||||||
|
public ulong Total;
|
||||||
|
public ulong Free;
|
||||||
|
public ulong Used;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
### Core Monitoring
|
||||||
- **Multi-core CPU monitoring** with per-core usage and frequency tracking
|
- **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection
|
||||||
- **Advanced memory monitoring** including paged/non-paged memory
|
- **Memory Monitoring**: RAM usage, available memory, committed memory, and paging
|
||||||
- **Enhanced GPU monitoring** with NVIDIA GPU support via NVML
|
- **GPU Monitoring**: NVIDIA GPU usage, memory utilization, temperature, fan speed, and power consumption (via NVML)
|
||||||
- **Comprehensive disk monitoring** with I/O performance metrics
|
- **Disk Monitoring**: I/O statistics, space usage, and performance counters
|
||||||
- **Network monitoring** with per-adapter statistics
|
- **Network Monitoring**: Bandwidth usage, packet statistics, and interface data
|
||||||
- **Temperature monitoring** for CPU, GPU, and storage devices
|
- **Temperature Monitoring**: CPU and hard drive temperature sensors
|
||||||
- **Process monitoring** with detailed resource usage tracking
|
|
||||||
|
|
||||||
### VM-Specific Features
|
### VM-Specific Features
|
||||||
- **Hypervisor detection** (VMware, Hyper-V, QEMU/KVM, etc.)
|
- **VM Detection**: Automatically detects virtualization environment
|
||||||
- **VM information** including boot time and uptime tracking
|
- **Hypervisor Identification**: Identifies VMware, VirtualBox, Hyper-V, KVM, etc.
|
||||||
- **Virtual machine optimization** for better performance in virtualized environments
|
- **Unraid Optimization**: Optimized for Unraid VM environments
|
||||||
|
- **Resource Alerting**: Configurable thresholds for resource usage alerts
|
||||||
|
|
||||||
### Game Detection & Management
|
### Advanced Features
|
||||||
- **Multi-platform game detection** (Steam, Epic Games, GOG, Origin, Ubisoft)
|
- **Game Detection**: Multi-platform game detection with fullscreen monitoring and configurable root folders
|
||||||
- **Fullscreen detection** for gaming sessions
|
- **Process Management**: View top processes with CPU/memory percentages, terminate processes via API
|
||||||
- **Game performance monitoring** with memory and CPU usage
|
- **Smart Alerting**: Duration-based alerting to prevent false positives
|
||||||
- **Enhanced process management** with graceful termination options
|
- **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
|
## 📡 API Endpoints
|
||||||
- **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
|
|
||||||
|
|
||||||
### Improved API
|
The service runs on `http://localhost:5000` by default and provides the following endpoints:
|
||||||
- **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)
|
|
||||||
|
|
||||||
- **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
|
||||||
|
|
||||||
```
|
### Game Detection
|
||||||
ResourceUsageAPI/
|
- `GET /api/current-game` - Currently running game information
|
||||||
├── Worker.cs
|
- `GET /api/all-games` - All detected games on the system
|
||||||
├── Program.cs
|
- `GET /api/fullscreen-status` - Check if any game is running fullscreen
|
||||||
├── Startup.cs
|
|
||||||
└── NvmlWrapper.cs
|
### 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.
|
Configuration is managed through `appsettings.json`:
|
||||||
- **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.
|
|
||||||
|
|
||||||
### 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`.
|
The service supports advanced game detection through two complementary approaches:
|
||||||
- **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.
|
|
||||||
|
|
||||||
### 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`.
|
```json
|
||||||
- **Methods**:
|
"GameRootFolders": [
|
||||||
- `ConfigureServices`: Placeholder method for adding services.
|
"C:\\Games",
|
||||||
- `Configure`: Placeholder method for configuring application HTTP requests pipeline.
|
"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`.
|
D:\Games\The Witcher 3\witcher3.exe
|
||||||
- **Methods**:
|
→ Game Name: "The Witcher 3"
|
||||||
- Importing NVML DLL functions to interact with GPU hardware.
|
→ Platform: "Standalone"
|
||||||
- Structures like `NvmlUtilization` are defined for handling utilization rates returned by NVML.
|
```
|
||||||
|
|
||||||
## 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.
|
## 📊 Example API Responses
|
||||||
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"`.
|
|
||||||
|
|
||||||
## 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**:
|
### CPU Usage
|
||||||
- URL: `/api/resource-usage`
|
```json
|
||||||
- Method: GET
|
{
|
||||||
- Description: Retrieves current system resource usage.
|
"usage": 15.5,
|
||||||
|
"coreUsages": [12.1, 18.3, 14.7, 16.2],
|
||||||
|
"temperature": 65.0,
|
||||||
|
"maxFrequency": 4400,
|
||||||
|
"currentFrequency": 3200,
|
||||||
|
"isThrottling": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **Kill Process**:
|
### VM Information
|
||||||
- URL: `/api/kill-process`
|
```json
|
||||||
- Method: POST
|
{
|
||||||
- Body: JSON with the process ID (`{"id": "1234"}`)
|
"isVirtualMachine": true,
|
||||||
- Description: Kills the specified process.
|
"hypervisorVendor": "VMware",
|
||||||
|
"uptime": "1.16:55:30",
|
||||||
|
"bootTime": "2025-08-05T09:34:04Z",
|
||||||
|
"machineName": "WIN11-VM",
|
||||||
|
"domain": "WORKGROUP"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Important Notes
|
### 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": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Ensure that the NVIDIA Management Library (NVML) is installed on the system for GPU monitoring to work.
|
### Game Detection
|
||||||
- The application allows CORS from all origins, which should be configured securely in production environments.
|
```json
|
||||||
- Error handling and logging are minimal; consider adding robust error handling and logging mechanisms for a production-ready solution.
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
### 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Feel free to contribute by opening issues or submitting pull requests. Make sure to follow the project's coding style and best practices.
|
## 🔧 PowerShell Usage Examples
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Get system health
|
||||||
|
$health = Invoke-RestMethod -Uri "http://localhost:5000/api/health"
|
||||||
|
Write-Host "System Status: $($health.status)"
|
||||||
|
|
||||||
# devnote
|
# Get CPU usage
|
||||||
dotnet run
|
$cpu = Invoke-RestMethod -Uri "http://localhost:5000/api/cpu-usage"
|
||||||
git add .
|
Write-Host "CPU Usage: $($cpu.usage)%"
|
||||||
git commit -m "Add steam running games"
|
|
||||||
git push origin master
|
|
||||||
dotnet publish -c Release -o ./publish
|
|
||||||
|
|
||||||
# devtest
|
# Get GPU usage
|
||||||
Invoke-WebRequest -Uri "http://localhost:5000/api/kill-process" -Method POST -Body "1234" -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" }
|
$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)"
|
||||||
|
}
|
||||||
|
|
||||||
Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/resource-usage" -Method GET -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" }
|
# 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"
|
||||||
|
}
|
||||||
|
|
||||||
Use 'shutdown', 'restart', or 'cancel'.
|
# Get all detected games
|
||||||
Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/force-shutdown" `
|
$allGames = Invoke-RestMethod -Uri "http://localhost:5000/api/all-games"
|
||||||
-Method POST `
|
Write-Host "Detected games on system:"
|
||||||
-Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } `
|
foreach ($gameItem in $allGames) {
|
||||||
-Body '{"Action": "shutdown", "DelaySeconds": 120}' `
|
Write-Host " $($gameItem.gameName) ($($gameItem.platform)) - Memory: $([math]::Round($gameItem.memoryUsage / 1MB, 0))MB"
|
||||||
-ContentType "application/json"
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
-248
@@ -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
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-ResourceMonitorService-ff17df27-9a94-433d-84e9-744dd4b626c2</UserSecretsId>
|
<UserSecretsId>dotnet-ResourceMonitorService-ff17df27-9a94-433d-84e9-744dd4b626c2</UserSecretsId>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -13,7 +14,6 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.0" />
|
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Windows.Forms" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
|
|||||||
@@ -201,6 +201,13 @@ namespace ResourceMonitorService.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// First check configured game root folders
|
||||||
|
var gameFromRootFolder = DetectGameFromRootFolders(filePath, process);
|
||||||
|
if (gameFromRootFolder != null)
|
||||||
|
{
|
||||||
|
return gameFromRootFolder;
|
||||||
|
}
|
||||||
|
|
||||||
// Check each configured game platform path
|
// Check each configured game platform path
|
||||||
foreach (var platformPath in _settings.GamePlatformPaths)
|
foreach (var platformPath in _settings.GamePlatformPaths)
|
||||||
{
|
{
|
||||||
@@ -227,10 +234,28 @@ namespace ResourceMonitorService.Services
|
|||||||
|
|
||||||
// Additional checks for common game launchers and executables
|
// Additional checks for common game launchers and executables
|
||||||
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
|
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[]
|
var knownGameExecutables = new[]
|
||||||
{
|
{
|
||||||
"game", "launcher", "client", "main", "start", "run",
|
"game", "launcher", "client"
|
||||||
// Add more common game executable patterns
|
// Removed generic terms like "main", "start", "run" that match too many system processes
|
||||||
};
|
};
|
||||||
|
|
||||||
var gameIndicators = new[]
|
var gameIndicators = new[]
|
||||||
@@ -240,8 +265,10 @@ namespace ResourceMonitorService.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if it's likely a game based on executable name or path
|
// Check if it's likely a game based on executable name or path
|
||||||
if (knownGameExecutables.Any(exe => fileName.Contains(exe)) ||
|
// Made the condition more restrictive to reduce false positives
|
||||||
gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase)))
|
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
|
// Try to determine platform and game name from other indicators
|
||||||
var platform = DeterminePlatformFromProcess(process, filePath);
|
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)
|
private int GetParentProcessId(int processId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ namespace ResourceMonitorService.Services
|
|||||||
private readonly Dictionary<string, PerformanceCounter> _counters;
|
private readonly Dictionary<string, PerformanceCounter> _counters;
|
||||||
private readonly Dictionary<string, long> _previousNetworkBytes;
|
private readonly Dictionary<string, long> _previousNetworkBytes;
|
||||||
private readonly Dictionary<string, DateTime> _previousNetworkTime;
|
private readonly Dictionary<string, DateTime> _previousNetworkTime;
|
||||||
|
private readonly Dictionary<string, long> _previousDiskBytes;
|
||||||
|
private readonly Dictionary<string, DateTime> _previousDiskTime;
|
||||||
|
private readonly Dictionary<string, int> _errorCounts;
|
||||||
|
private readonly Dictionary<int, (TimeSpan ProcessorTime, DateTime Timestamp)> _previousProcessorTimes;
|
||||||
|
|
||||||
public ResourceMonitorService(ILogger<ResourceMonitorService> logger, IOptions<MonitoringSettings> settings)
|
public ResourceMonitorService(ILogger<ResourceMonitorService> logger, IOptions<MonitoringSettings> settings)
|
||||||
{
|
{
|
||||||
@@ -33,6 +37,10 @@ namespace ResourceMonitorService.Services
|
|||||||
_counters = new Dictionary<string, PerformanceCounter>();
|
_counters = new Dictionary<string, PerformanceCounter>();
|
||||||
_previousNetworkBytes = new Dictionary<string, long>();
|
_previousNetworkBytes = new Dictionary<string, long>();
|
||||||
_previousNetworkTime = new Dictionary<string, DateTime>();
|
_previousNetworkTime = new Dictionary<string, DateTime>();
|
||||||
|
_previousDiskBytes = new Dictionary<string, long>();
|
||||||
|
_previousDiskTime = new Dictionary<string, DateTime>();
|
||||||
|
_errorCounts = new Dictionary<string, int>();
|
||||||
|
_previousProcessorTimes = new Dictionary<int, (TimeSpan ProcessorTime, DateTime Timestamp)>();
|
||||||
InitializeCounters();
|
InitializeCounters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +67,7 @@ namespace ResourceMonitorService.Services
|
|||||||
#pragma warning restore CA1416 // Validate platform compatibility
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
|
|
||||||
// Initialize counters with first reading
|
// Initialize counters with first reading
|
||||||
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
foreach (var counter in _counters.Values)
|
foreach (var counter in _counters.Values)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -67,9 +76,10 @@ namespace ResourceMonitorService.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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)
|
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<ResourceUsage> GetResourceUsageAsync()
|
public async Task<ResourceUsage> GetResourceUsageAsync()
|
||||||
{
|
{
|
||||||
var timestamp = DateTime.Now;
|
var timestamp = DateTime.Now;
|
||||||
@@ -274,27 +300,46 @@ namespace ResourceMonitorService.Services
|
|||||||
uint temperature;
|
uint temperature;
|
||||||
NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature);
|
NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature);
|
||||||
|
|
||||||
uint fanSpeed;
|
uint fanSpeed = 0;
|
||||||
NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed);
|
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 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 driverVersion = "Unknown";
|
||||||
var memoryTotal = 0UL;
|
|
||||||
var memoryUsed = 0UL;
|
|
||||||
var powerUsage = 0U;
|
|
||||||
|
|
||||||
try
|
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
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController WHERE Name LIKE '%NVIDIA%'");
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController WHERE Name LIKE '%NVIDIA%'");
|
||||||
using var collection = searcher.Get();
|
using var collection = searcher.Get();
|
||||||
foreach (ManagementObject obj in collection)
|
foreach (ManagementObject obj in collection)
|
||||||
{
|
{
|
||||||
name = obj["Name"]?.ToString() ?? name;
|
|
||||||
driverVersion = obj["DriverVersion"]?.ToString() ?? driverVersion;
|
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"];
|
memoryTotal = (ulong)obj["AdapterRAM"];
|
||||||
}
|
}
|
||||||
@@ -304,7 +349,7 @@ namespace ResourceMonitorService.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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();
|
NvmlWrapper.NvmlShutdown();
|
||||||
@@ -315,7 +360,7 @@ namespace ResourceMonitorService.Services
|
|||||||
MemoryUsage = utilization.Memory,
|
MemoryUsage = utilization.Memory,
|
||||||
Temperature = temperature,
|
Temperature = temperature,
|
||||||
FanSpeed = fanSpeed,
|
FanSpeed = fanSpeed,
|
||||||
PowerUsage = powerUsage,
|
PowerUsage = powerUsage, // Power in milliwatts
|
||||||
MemoryTotal = memoryTotal,
|
MemoryTotal = memoryTotal,
|
||||||
MemoryUsed = memoryUsed,
|
MemoryUsed = memoryUsed,
|
||||||
IsAvailable = true,
|
IsAvailable = true,
|
||||||
@@ -357,6 +402,7 @@ namespace ResourceMonitorService.Services
|
|||||||
return await Task.Run(() =>
|
return await Task.Run(() =>
|
||||||
{
|
{
|
||||||
var diskUsages = new List<DiskUsage>();
|
var diskUsages = new List<DiskUsage>();
|
||||||
|
var timestamp = DateTime.Now;
|
||||||
|
|
||||||
var drives = DriveInfo.GetDrives();
|
var drives = DriveInfo.GetDrives();
|
||||||
foreach (var drive in drives)
|
foreach (var drive in drives)
|
||||||
@@ -374,50 +420,14 @@ namespace ResourceMonitorService.Services
|
|||||||
UsagePercentage = (float)(drive.TotalSize - drive.AvailableFreeSpace) / drive.TotalSize * 100
|
UsagePercentage = (float)(drive.TotalSize - drive.AvailableFreeSpace) / drive.TotalSize * 100
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get disk performance data
|
// Get disk performance data with proper timing
|
||||||
try
|
GetDiskPerformanceData(drive, diskUsage, timestamp);
|
||||||
{
|
|
||||||
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();
|
// Get disk temperature using SMART data
|
||||||
writeCounter.NextValue();
|
GetDiskTemperature(drive, diskUsage);
|
||||||
timeCounter.NextValue();
|
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
// Get additional disk information
|
||||||
|
GetDiskInfo(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
diskUsages.Add(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<NetworkUsage> GetNetworkUsageAsync()
|
public async Task<NetworkUsage> GetNetworkUsageAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -524,35 +745,89 @@ namespace ResourceMonitorService.Services
|
|||||||
{
|
{
|
||||||
return await Task.Run(() =>
|
return await Task.Run(() =>
|
||||||
{
|
{
|
||||||
var processes = Process.GetProcesses()
|
var allProcesses = Process.GetProcesses();
|
||||||
.Where(p => !p.HasExited)
|
_logger.LogDebug("Found {ProcessCount} total processes", allProcesses.Length);
|
||||||
.Select(p =>
|
|
||||||
|
var validProcesses = new List<ProcessInfo>();
|
||||||
|
int skippedCount = 0;
|
||||||
|
var currentTime = DateTime.Now;
|
||||||
|
|
||||||
|
// Get total system memory for percentage calculations
|
||||||
|
var totalSystemMemory = GetTotalSystemMemory();
|
||||||
|
|
||||||
|
foreach (var p in allProcesses)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new ProcessInfo
|
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,
|
Id = p.Id,
|
||||||
Name = p.ProcessName,
|
Name = p.ProcessName,
|
||||||
MemoryUsage = (ulong)p.WorkingSet64,
|
MemoryUsage = memoryUsage,
|
||||||
|
MemoryUsagePercentage = totalSystemMemory > 0 ? (float)(memoryUsage * 100.0 / totalSystemMemory) : 0f,
|
||||||
ProcessorTime = p.TotalProcessorTime,
|
ProcessorTime = p.TotalProcessorTime,
|
||||||
StartTime = p.StartTime,
|
CpuUsage = cpuUsage
|
||||||
ExecutablePath = p.MainModule?.FileName ?? "",
|
|
||||||
CommandLine = GetProcessCommandLine(p.Id)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Try to get additional info, but don't fail if we can't
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processInfo.StartTime = p.StartTime;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null; // Skip processes that throw exceptions
|
processInfo.StartTime = DateTime.MinValue;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.Where(p => p != null)
|
try
|
||||||
.OrderByDescending(p => p!.MemoryUsage)
|
{
|
||||||
|
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)
|
.Take(count)
|
||||||
.Cast<ProcessInfo>()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return processes;
|
_logger.LogDebug("Returning {TopCount} top processes", topProcesses.Count);
|
||||||
|
return topProcesses;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -583,7 +858,7 @@ namespace ResourceMonitorService.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not get CPU temperature");
|
LogSuppressedWarning("cpu_temperature", ex, "Could not get CPU temperature");
|
||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,28 +875,95 @@ namespace ResourceMonitorService.Services
|
|||||||
HardDrives = new List<HardDriveTemp>()
|
HardDrives = new List<HardDriveTemp>()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to get hard drive temperatures
|
// Get hard drive temperatures using improved method
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var drives = DriveInfo.GetDrives();
|
||||||
|
foreach (var drive in drives.Where(d => d.IsReady && d.DriveType == DriveType.Fixed))
|
||||||
|
{
|
||||||
|
var hardDriveTemp = new HardDriveTemp
|
||||||
|
{
|
||||||
|
Drive = drive.Name,
|
||||||
|
Temperature = 0f,
|
||||||
|
Health = "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the same SMART data access as in GetDiskTemperature
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
#pragma warning disable CA1416 // Validate platform compatibility
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
using var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
|
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive");
|
||||||
using var collection = searcher.Get();
|
using var collection = searcher.Get();
|
||||||
foreach (ManagementObject obj in collection)
|
foreach (ManagementObject disk in collection)
|
||||||
{
|
{
|
||||||
var instanceName = obj["InstanceName"]?.ToString() ?? "";
|
var model = disk["Model"]?.ToString() ?? "";
|
||||||
// This would need more sophisticated parsing for actual SMART data
|
|
||||||
temperatureInfo.HardDrives.Add(new HardDriveTemp
|
// Try to get SMART data using MSStorageDriver_ATAPISmartData
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Drive = instanceName,
|
using var smartSearcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
|
||||||
Temperature = 0f, // Would need SMART data parsing
|
using var smartCollection = smartSearcher.Get();
|
||||||
Health = "Unknown"
|
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
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not get hard drive temperatures");
|
LogSuppressedWarning($"hdd_temp_info_{drive.Name}", ex, $"Could not get temperature info for drive {drive.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
temperatureInfo.HardDrives.Add(hardDriveTemp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogSuppressedWarning("hdd_temperature", ex, "Could not get hard drive temperatures");
|
||||||
}
|
}
|
||||||
|
|
||||||
return temperatureInfo;
|
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<int> currentProcessIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Remove entries for processes that no longer exist
|
||||||
|
var keysToRemove = _previousProcessorTimes.Keys
|
||||||
|
.Where(pid => !currentProcessIds.Contains(pid))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
_previousProcessorTimes.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also remove very old entries (older than 5 minutes) to prevent indefinite growth
|
||||||
|
var cutoffTime = DateTime.Now.AddMinutes(-5);
|
||||||
|
var oldKeys = _previousProcessorTimes
|
||||||
|
.Where(kvp => kvp.Value.Timestamp < cutoffTime)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in oldKeys)
|
||||||
|
{
|
||||||
|
_previousProcessorTimes.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keysToRemove.Count > 0 || oldKeys.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Cleaned up {RemovedCount} old process entries, {OldCount} expired entries",
|
||||||
|
keysToRemove.Count, oldKeys.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogSuppressedWarning("cleanup_process", ex, "Error cleaning up old process entries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong GetTotalSystemMemory()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
|
using var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
|
||||||
|
using var collection = searcher.Get();
|
||||||
|
foreach (ManagementObject obj in collection)
|
||||||
|
{
|
||||||
|
return (ulong)obj["TotalPhysicalMemory"];
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
|
return 0UL;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogSuppressedWarning("total_memory", ex, "Could not get total system memory");
|
||||||
|
return 0UL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
#pragma warning disable CA1416 // Validate platform compatibility
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
|
|||||||
-387
@@ -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<Worker> _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<Worker> logger,
|
|
||||||
IHostApplicationLifetime lifetime,
|
|
||||||
ISystemInfoService systemInfoService,
|
|
||||||
IResourceMonitorService resourceMonitorService,
|
|
||||||
IGameDetectionService gameDetectionService,
|
|
||||||
IAlertService alertService,
|
|
||||||
IOptions<ApiSettings> apiSettings,
|
|
||||||
IOptions<MonitoringSettings> 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<dynamic>(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<dynamic>(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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-1
@@ -2,7 +2,10 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information",
|
||||||
|
"ResourceMonitorService.Services.ResourceMonitorService": "Error",
|
||||||
|
"ResourceMonitorService.Services.GameDetectionService": "Error",
|
||||||
|
"System": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RunAsWindowsService": true,
|
"RunAsWindowsService": true,
|
||||||
@@ -44,6 +47,11 @@
|
|||||||
"\\Origin Games\\",
|
"\\Origin Games\\",
|
||||||
"\\Ubisoft Game Launcher\\games\\"
|
"\\Ubisoft Game Launcher\\games\\"
|
||||||
],
|
],
|
||||||
|
"GameRootFolders": [
|
||||||
|
"C:\\Games",
|
||||||
|
"D:\\Games",
|
||||||
|
"E:\\Games"
|
||||||
|
],
|
||||||
"AlertThresholds": [
|
"AlertThresholds": [
|
||||||
{
|
{
|
||||||
"Component": "CPU",
|
"Component": "CPU",
|
||||||
|
|||||||
Reference in New Issue
Block a user