Update project files and configurations for improved structure and maintainability

This commit is contained in:
Phoenix
2025-08-07 16:53:10 +08:00
parent 3b3bdf3d46
commit 3f64ace8a7
11 changed files with 1062 additions and 837 deletions
+4
View File
@@ -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
+7
View File
@@ -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 },
+1
View File
@@ -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;
+17
View File
@@ -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;
}
} }
+385 -107
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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" />
+109 -4
View File
@@ -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
+527 -87
View File
@@ -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
{ {
if (p.HasExited)
{
skippedCount++;
continue;
}
var memoryUsage = (ulong)p.WorkingSet64;
var cpuUsage = CalculateProcessCpuUsage(p.Id, p.TotalProcessorTime, currentTime);
var processInfo = new ProcessInfo
{
Id = p.Id,
Name = p.ProcessName,
MemoryUsage = memoryUsage,
MemoryUsagePercentage = totalSystemMemory > 0 ? (float)(memoryUsage * 100.0 / totalSystemMemory) : 0f,
ProcessorTime = p.TotalProcessorTime,
CpuUsage = cpuUsage
};
// Try to get additional info, but don't fail if we can't
try try
{ {
return new ProcessInfo processInfo.StartTime = p.StartTime;
{
Id = p.Id,
Name = p.ProcessName,
MemoryUsage = (ulong)p.WorkingSet64,
ProcessorTime = p.TotalProcessorTime,
StartTime = p.StartTime,
ExecutablePath = p.MainModule?.FileName ?? "",
CommandLine = GetProcessCommandLine(p.Id)
};
} }
catch 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 try
{ {
#pragma warning disable CA1416 // Validate platform compatibility var drives = DriveInfo.GetDrives();
using var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData"); foreach (var drive in drives.Where(d => d.IsReady && d.DriveType == DriveType.Fixed))
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{ {
var instanceName = obj["InstanceName"]?.ToString() ?? ""; var hardDriveTemp = new HardDriveTemp
// This would need more sophisticated parsing for actual SMART data
temperatureInfo.HardDrives.Add(new HardDriveTemp
{ {
Drive = instanceName, Drive = drive.Name,
Temperature = 0f, // Would need SMART data parsing Temperature = 0f,
Health = "Unknown" Health = "Unknown"
}); };
}
// Use the same SMART data access as in GetDiskTemperature
try
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive");
using var collection = searcher.Get();
foreach (ManagementObject disk in collection)
{
var model = disk["Model"]?.ToString() ?? "";
// Try to get SMART data using MSStorageDriver_ATAPISmartData
try
{
using var smartSearcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM MSStorageDriver_ATAPISmartData");
using var smartCollection = smartSearcher.Get();
foreach (ManagementObject smartData in smartCollection)
{
var instanceName = smartData["InstanceName"]?.ToString() ?? "";
if (instanceName.Contains(drive.Name.Replace("\\", "").Replace(":", "")))
{
var vendorSpecific = smartData["VendorSpecific"] as byte[];
if (vendorSpecific != null && vendorSpecific.Length >= 362)
{
// Parse SMART attributes for temperature (attribute 194)
for (int i = 2; i < 362; i += 12)
{
if (i + 11 < vendorSpecific.Length && vendorSpecific[i] == 194)
{
hardDriveTemp.Temperature = vendorSpecific[i + 5];
break;
}
}
// Parse SMART attributes for health indicators
bool hasWarnings = false;
for (int i = 2; i < 362; i += 12)
{
if (i + 11 < vendorSpecific.Length)
{
var attributeId = vendorSpecific[i];
var threshold = vendorSpecific[i + 2];
var value = vendorSpecific[i + 3];
// Check critical attributes
if ((attributeId == 5 || attributeId == 196 || attributeId == 197 || attributeId == 198) && value <= threshold)
{
hasWarnings = true;
break;
}
}
}
hardDriveTemp.Health = hasWarnings ? "Warning" : "Good";
}
break;
}
}
}
catch (Exception ex)
{
LogSuppressedWarning($"smart_temp_info_{drive.Name}", ex, $"Could not get SMART temperature info for {drive.Name}");
}
}
#pragma warning restore CA1416 // Validate platform compatibility #pragma warning restore CA1416 // Validate platform compatibility
}
catch (Exception ex)
{
LogSuppressedWarning($"hdd_temp_info_{drive.Name}", ex, $"Could not get temperature info for drive {drive.Name}");
}
temperatureInfo.HardDrives.Add(hardDriveTemp);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Could not get hard drive temperatures"); 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
View File
@@ -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
View File
@@ -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",