6 Commits

Author SHA1 Message Date
Phoenix f2a0818d0e Update service port from 5000 to 2414 and adjust related configurations 2025-08-07 17:53:54 +08:00
Phoenix d6efa9163b Add Telegram bot integration for real-time alert notifications
- Implemented ITelegramNotificationService and TelegramNotificationService for sending alerts via Telegram.
- Updated MonitoringSettings to include Telegram configuration options.
- Enhanced AlertService to send alerts and resolutions through Telegram.
- Added API endpoints for checking Telegram status and sending test alerts.
- Updated README and TELEGRAM_SETUP.md with setup instructions and features.
- Included example configuration in appsettings.telegram.example.json.
2025-08-07 17:30:02 +08:00
king 774cdbaf66 Merge pull request 'feature/ai-resource-monitor' (#3) from feature/ai-resource-monitor into master
Reviewed-on: #3
2025-08-07 16:57:28 +08:00
Phoenix 3f64ace8a7 Update project files and configurations for improved structure and maintainability 2025-08-07 16:53:10 +08:00
Phoenix 3b3bdf3d46 Implement code changes to enhance functionality and improve performance 2025-08-07 16:52:55 +08:00
Phoenix 823e467078 Add start-service.bat script for Resource Monitor Service v2.0
- Introduced a batch script to simplify the startup process for the Resource Monitor Service.
- Included checks for .NET 9.0 Runtime installation.
- Added build and run commands for the service with appropriate error handling.
- Provided user instructions and API documentation links in the script output.
2025-08-07 02:39:54 +08:00
27 changed files with 4259 additions and 538 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
+90
View File
@@ -0,0 +1,90 @@
namespace ResourceMonitorService.Configuration
{
public class MonitoringSettings
{
public int UpdateIntervalMs { get; set; } = 5000; // 5 seconds
public int DataRetentionDays { get; set; } = 7;
public bool EnableGpuMonitoring { get; set; } = true;
public bool EnableDiskMonitoring { get; set; } = true;
public bool EnableNetworkMonitoring { get; set; } = true;
public bool EnableTemperatureMonitoring { get; set; } = true;
public bool EnableProcessMonitoring { get; set; } = true;
public bool EnableGameDetection { get; set; } = true;
public bool EnableAlerts { get; set; } = true;
public int MaxProcessesToTrack { get; set; } = 10;
public int MaxHistoryPoints { get; set; } = 1000;
public List<string> GamePlatformPaths { get; set; } = new()
{
@"\steamapps\common\",
@"\Epic Games\",
@"\GOG Galaxy\Games\",
@"\Origin 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()
{
new() { Component = "CPU", WarningThreshold = 80, CriticalThreshold = 95, DurationSeconds = 30 },
new() { Component = "Memory", WarningThreshold = 85, CriticalThreshold = 95, DurationSeconds = 30 },
new() { Component = "GPU", WarningThreshold = 85, CriticalThreshold = 95, DurationSeconds = 30 },
new() { Component = "CPUTemp", WarningThreshold = 75, CriticalThreshold = 85, DurationSeconds = 60 },
new() { Component = "GPUTemp", WarningThreshold = 80, CriticalThreshold = 90, DurationSeconds = 60 }
};
public TelegramSettings Telegram { get; set; } = new();
}
public class AlertThresholdConfig
{
public string Component { get; set; } = string.Empty;
public float WarningThreshold { get; set; }
public float CriticalThreshold { get; set; }
public int DurationSeconds { get; set; } = 30;
public bool IsEnabled { get; set; } = true;
}
public class ApiSettings
{
public string ApiKey { get; set; } = string.Empty;
public bool RequireApiKey { get; set; } = false;
public List<string> AllowedOrigins { get; set; } = new()
{
"http://localhost:4200",
"http://192.168.50.52:4200",
"http://vmwin11:4200"
};
public bool EnableSwagger { get; set; } = false;
public string BasePath { get; set; } = "/api";
}
public class LoggingSettings
{
public string LogLevel { get; set; } = "Information";
public string LogPath { get; set; } = "logs";
public int MaxLogFiles { get; set; } = 30;
public long MaxLogFileSizeMB { get; set; } = 10;
public bool EnableFileLogging { get; set; } = true;
public bool EnableConsoleLogging { get; set; } = true;
public bool EnablePerformanceLogging { get; set; } = false;
}
public class TelegramSettings
{
public bool IsEnabled { get; set; } = false;
public string BotToken { get; set; } = string.Empty;
public List<long> ChatIds { get; set; } = new();
public bool SendWarningAlerts { get; set; } = true;
public bool SendCriticalAlerts { get; set; } = true;
public bool SendResolutionNotifications { get; set; } = true;
public string MessageTemplate { get; set; } = "🚨 *{Level} Alert*\n\n📊 *{Component}*\n💬 {Message}\n⏰ {Timestamp:yyyy-MM-dd HH:mm:ss}";
public string ResolutionTemplate { get; set; } = "✅ *Alert Resolved*\n\n📊 *{Component}*\n💬 {Message}\n⏰ Resolved at {ResolvedAt:yyyy-MM-dd HH:mm:ss}";
}
}
+171
View File
@@ -0,0 +1,171 @@
using System.ComponentModel.DataAnnotations;
namespace ResourceMonitorService.Models
{
public class SystemInfo
{
public string MachineName { get; set; } = string.Empty;
public string OSVersion { get; set; } = string.Empty;
public string OSArchitecture { get; set; } = string.Empty;
public int ProcessorCount { get; set; }
public ulong TotalPhysicalMemory { get; set; }
public string CPUName { get; set; } = string.Empty;
public DateTime BootTime { get; set; }
public TimeSpan Uptime { get; set; }
public string Domain { get; set; } = string.Empty;
public bool IsVirtualMachine { get; set; }
public string HypervisorVendor { get; set; } = string.Empty;
}
public class ResourceUsage
{
public DateTime Timestamp { get; set; }
public CpuUsage CPU { get; set; } = new();
public MemoryUsage Memory { get; set; } = new();
public GpuUsage GPU { get; set; } = new();
public List<DiskUsage> Disks { get; set; } = new();
public NetworkUsage Network { get; set; } = new();
public List<ProcessInfo> TopProcesses { get; set; } = new();
public TemperatureInfo Temperature { get; set; } = new();
public GameInfo? RunningGame { get; set; }
}
public class CpuUsage
{
public float Usage { get; set; }
public float[] CoreUsages { get; set; } = Array.Empty<float>();
public float Temperature { get; set; }
public float MaxFrequency { get; set; }
public float CurrentFrequency { get; set; }
public bool IsThrottling { get; set; }
}
public class MemoryUsage
{
public float UsagePercentage { get; set; }
public ulong UsedMemory { get; set; }
public ulong AvailableMemory { get; set; }
public ulong TotalMemory { get; set; }
public ulong CommittedMemory { get; set; }
public ulong PagedMemory { get; set; }
public ulong NonPagedMemory { get; set; }
}
public class GpuUsage
{
public uint Usage { get; set; }
public uint MemoryUsage { get; set; }
public uint Temperature { get; set; }
public uint FanSpeed { get; set; }
public uint PowerUsage { get; set; }
public ulong MemoryTotal { get; set; }
public ulong MemoryUsed { get; set; }
public bool IsAvailable { get; set; }
public string Error { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DriverVersion { get; set; } = string.Empty;
}
public class DiskUsage
{
public string DriveLetter { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string FileSystem { get; set; } = string.Empty;
public ulong TotalSize { get; set; }
public ulong FreeSpace { get; set; }
public ulong UsedSpace { get; set; }
public float UsagePercentage { get; set; }
public float ReadSpeed { get; set; } // MB/s
public float WriteSpeed { get; set; } // MB/s
public float DiskTime { get; set; } // % disk time
public uint Temperature { get; set; }
public bool IsSSD { get; set; }
public long ReadOperations { get; set; }
public long WriteOperations { get; set; }
}
public class NetworkUsage
{
public float UploadSpeed { get; set; } // MB/s
public float DownloadSpeed { get; set; } // MB/s
public ulong BytesSent { get; set; }
public ulong BytesReceived { get; set; }
public List<NetworkAdapter> Adapters { get; set; } = new();
}
public class NetworkAdapter
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsOperational { get; set; }
public long Speed { get; set; }
public float UploadSpeed { get; set; }
public float DownloadSpeed { get; set; }
public string IPAddress { get; set; } = string.Empty;
public string MACAddress { get; set; } = string.Empty;
}
public class ProcessInfo
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public float CpuUsage { get; set; }
public ulong MemoryUsage { get; set; }
public float MemoryUsagePercentage { get; set; }
public TimeSpan ProcessorTime { get; set; }
public DateTime StartTime { get; set; }
public string ExecutablePath { get; set; } = string.Empty;
public string CommandLine { get; set; } = string.Empty;
}
public class TemperatureInfo
{
public float CPU { get; set; }
public float GPU { get; set; }
public float Motherboard { get; set; }
public List<HardDriveTemp> HardDrives { get; set; } = new();
}
public class HardDriveTemp
{
public string Drive { get; set; } = string.Empty;
public float Temperature { get; set; }
public string Health { get; set; } = string.Empty;
}
public class GameInfo
{
public string GameName { get; set; } = string.Empty;
public string ExecutableName { get; set; } = string.Empty;
public string FullPath { get; set; } = string.Empty;
public int ProcessId { get; set; }
public ulong MemoryUsage { get; set; }
public TimeSpan CpuTime { get; set; }
public DateTime StartTime { get; set; }
public string Platform { get; set; } = string.Empty; // Steam, Epic, etc.
public bool IsFullscreen { get; set; }
public float FPS { get; set; }
}
public class AlertThreshold
{
public string Name { get; set; } = string.Empty;
public string Component { get; set; } = string.Empty; // CPU, Memory, GPU, Disk, Network
public float WarningThreshold { get; set; }
public float CriticalThreshold { get; set; }
public bool IsEnabled { get; set; }
public TimeSpan Duration { get; set; } // How long the threshold must be exceeded
}
public class Alert
{
public DateTime Timestamp { get; set; }
public string Component { get; set; } = string.Empty;
public string Level { get; set; } = string.Empty; // Warning, Critical
public string Message { get; set; } = string.Empty;
public float CurrentValue { get; set; }
public float ThresholdValue { get; set; }
public bool IsResolved { get; set; }
public DateTime? ResolvedAt { get; set; }
}
}
+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;
}
} }
+45 -2
View File
@@ -1,5 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using ResourceMonitorService.Configuration;
using ResourceMonitorService.Services;
using Serilog;
using System.Diagnostics; using System.Diagnostics;
namespace ResourceMonitorService namespace ResourceMonitorService
@@ -8,20 +12,59 @@ namespace ResourceMonitorService
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
CreateHostBuilder(args).Build().Run(); // Configure Serilog early
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/resourcemonitor-.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
try
{
Log.Information("Starting Resource Monitor Service");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
} }
public static IHostBuilder CreateHostBuilder(string[] args) public static IHostBuilder CreateHostBuilder(string[] args)
{ {
var builder = Host.CreateDefaultBuilder(args) var builder = Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureServices((hostContext, services) => .ConfigureServices((hostContext, services) =>
{ {
// Bind configuration sections
services.Configure<MonitoringSettings>(
hostContext.Configuration.GetSection("MonitoringSettings"));
services.Configure<ApiSettings>(
hostContext.Configuration.GetSection("ApiSettings"));
services.Configure<LoggingSettings>(
hostContext.Configuration.GetSection("LoggingSettings"));
// Register services
services.AddSingleton<ISystemInfoService, SystemInfoService>();
services.AddSingleton<IResourceMonitorService, Services.ResourceMonitorService>();
services.AddSingleton<IGameDetectionService, GameDetectionService>();
services.AddSingleton<ITelegramNotificationService, TelegramNotificationService>();
services.AddSingleton<IAlertService, AlertService>();
// Register the main worker service
services.AddHostedService<Worker>(); services.AddHostedService<Worker>();
}); });
// Configure as Windows Service if requested
if (args.Contains("--windows-service") || Environment.GetEnvironmentVariable("RUN_AS_SERVICE") == "true") if (args.Contains("--windows-service") || Environment.GetEnvironmentVariable("RUN_AS_SERVICE") == "true")
{ {
builder.UseWindowsService(); builder.UseWindowsService(options =>
{
options.ServiceName = "ResourceMonitorService";
});
} }
return builder; return builder;
+430 -86
View File
@@ -1,119 +1,463 @@
# Resource Usage API # Resource Monitor Service for Unraid VM
This project is a background service developed using ASP.NET Core that monitors system resource usage, such as CPU, RAM, GPU, and running games. The service provides APIs to fetch the current resource usage and kill specified processes. 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 ## 🚀 Features
- **Resource Monitoring**: Fetches detailed information about the system's resources, including: ### Core Monitoring
- Current Time - **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection
- Computer Information (Machine Name, OS Version, Architecture, Processor Count) - **Memory Monitoring**: RAM usage, available memory, committed memory, and paging
- CPU Usage - **GPU Monitoring**: NVIDIA GPU usage, memory utilization, temperature, fan speed, and power consumption (via NVML)
- RAM Usage - **Disk Monitoring**: I/O statistics, space usage, and performance counters
- GPU Usage - **Network Monitoring**: Bandwidth usage, packet statistics, and interface data
- Currently Running Steam Games (if any) - **Temperature Monitoring**: CPU and hard drive temperature sensors
- **Process Management**: Provides an API to kill processes by their process ID. ### 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
## Directory Structure ### Advanced Features
- **Game Detection**: Multi-platform game detection with fullscreen monitoring and configurable root folders
- **Process Management**: View top processes with CPU/memory percentages, terminate processes via API
- **Smart Alerting**: Duration-based alerting to prevent false positives
- **Telegram Bot Integration**: Real-time alerts via Telegram bot with customizable notifications
- **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
``` ## 📡 API Endpoints
ResourceUsageAPI/
├── Worker.cs The service runs on `http://localhost:5000` by default and provides the following endpoints:
├── Program.cs
├── Startup.cs ### System Information
└── NvmlWrapper.cs - `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` - 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
- `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
### Telegram Bot Integration
- `GET /api/telegram/status` - Check Telegram bot status and connection
- `POST /api/telegram/test` - Send a test alert to verify bot configuration
### 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. ### Telegram Bot Alerts
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 The service supports real-time alert notifications via Telegram bot. To set up Telegram alerts:
- **Get Resource Usage**: 1. **Create a Telegram Bot** - Contact `@BotFather` on Telegram and create a new bot
- URL: `/api/resource-usage` 2. **Get Chat ID** - Send a message to your bot, then visit `https://api.telegram.org/bot<TOKEN>/getUpdates`
- Method: GET 3. **Configure Settings** - Add Telegram configuration to your `appsettings.json`:
- Description: Retrieves current system resource usage.
- **Kill Process**:
- URL: `/api/kill-process`
- Method: POST
- Body: JSON with the process ID (`{"id": "1234"}`)
- Description: Kills the specified process.
## Important Notes ```json
{
"MonitoringSettings": {
"Telegram": {
"IsEnabled": true,
"BotToken": "123456789:ABCdefGHIjklMNOpqrSTUvwxyz",
"ChatIds": [123456789, -987654321],
"SendWarningAlerts": true,
"SendCriticalAlerts": true,
"SendResolutionNotifications": true,
"MessageTemplate": "🚨 *{Level} Alert*\n\n📊 *{Component}*\n💬 {Message}\n⏰ {Timestamp}",
"ResolutionTemplate": "✅ *Alert Resolved*\n\n📊 *{Component}*\n💬 {Message}\n⏰ Resolved at {ResolvedAt}"
}
}
}
```
- Ensure that the NVIDIA Management Library (NVML) is installed on the system for GPU monitoring to work. **Features:**
- The application allows CORS from all origins, which should be configured securely in production environments. - **Multiple Chats**: Send alerts to multiple users/groups by adding chat IDs
- Error handling and logging are minimal; consider adding robust error handling and logging mechanisms for a production-ready solution. - **Customizable Templates**: Modify message format with placeholders for alert data
- **Alert Filtering**: Choose which alert levels to send (Warning/Critical)
- **Silent Notifications**: Warning alerts are sent silently, critical alerts with sound
- **Resolution Notifications**: Optional notifications when alerts are resolved
## Contributing 📋 For detailed setup instructions, see [TELEGRAM_SETUP.md](TELEGRAM_SETUP.md)
Feel free to contribute by opening issues or submitting pull requests. Make sure to follow the project's coding style and best practices. ## 📊 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
}
}
```
# devnote ### CPU Usage
dotnet run ```json
git add . {
git commit -m "Add steam running games" "usage": 15.5,
git push origin master "coreUsages": [12.1, 18.3, 14.7, 16.2],
dotnet publish -c Release -o ./publish "temperature": 65.0,
"maxFrequency": 4400,
"currentFrequency": 3200,
"isThrottling": false
}
```
# devtest ### VM Information
Invoke-WebRequest -Uri "http://localhost:5000/api/kill-process" -Method POST -Body "1234" -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } ```json
{
"isVirtualMachine": true,
"hypervisorVendor": "VMware",
"uptime": "1.16:55:30",
"bootTime": "2025-08-05T09:34:04Z",
"machineName": "WIN11-VM",
"domain": "WORKGROUP"
}
```
Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/resource-usage" -Method GET -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } ### 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": ""
}
```
Use 'shutdown', 'restart', or 'cancel'. ### Game Detection
Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/force-shutdown" ` ```json
-Method POST ` {
-Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } ` "gameName": "Cyberpunk 2077",
-Body '{"Action": "shutdown", "DelaySeconds": 120}' ` "executableName": "Cyberpunk2077.exe",
-ContentType "application/json" "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
}
```
### Top Processes
```json
{
"value": [
{
"id": 11820,
"name": "WmiPrvSE",
"cpuUsage": 2.7276263,
"memoryUsage": 83120128,
"memoryUsagePercentage": 0.12576005,
"processorTime": "00:26:30.2500000",
"startTime": "2025-08-05T09:38:38.9837995+08:00",
"executablePath": "C:\\WINDOWS\\system32\\wbem\\wmiprvse.exe",
"commandLine": "C:\\WINDOWS\\system32\\wbem\\wmiprvse.exe"
},
{
"id": 8376,
"name": "explorer",
"cpuUsage": 1.5750673,
"memoryUsage": 403636224,
"memoryUsagePercentage": 0.61069816,
"processorTime": "00:24:36.7968750",
"startTime": "2025-08-07T15:26:31.096813+08:00",
"executablePath": "C:\\WINDOWS\\Explorer.EXE",
"commandLine": "C:\\WINDOWS\\Explorer.EXE"
}
],
"count": 2
}
```
## 🔧 PowerShell Usage Examples
```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 GPU usage
$gpu = Invoke-RestMethod -Uri "http://localhost:5000/api/gpu-usage"
if ($gpu.isAvailable) {
Write-Host "GPU: $($gpu.name)"
Write-Host "GPU Usage: $($gpu.usage)%"
Write-Host "GPU Memory: $([math]::Round($gpu.memoryUsed / 1GB, 2))GB / $([math]::Round($gpu.memoryTotal / 1GB, 2))GB ($($gpu.memoryUsage)%)"
Write-Host "GPU Temperature: $($gpu.temperature)°C"
} else {
Write-Host "GPU not available: $($gpu.error)"
}
# Get current game (enhanced with root folder detection)
$game = Invoke-RestMethod -Uri "http://localhost:5000/api/current-game"
if ($game) {
Write-Host "Currently playing: $($game.gameName)"
Write-Host "Platform: $($game.platform)"
Write-Host "Executable: $($game.executableName)"
if ($game.isFullscreen) {
Write-Host "Running in fullscreen mode"
}
} else {
Write-Host "No game currently detected"
}
# Get all detected games
$allGames = Invoke-RestMethod -Uri "http://localhost:5000/api/all-games"
Write-Host "Detected games on system:"
foreach ($gameItem in $allGames) {
Write-Host " $($gameItem.gameName) ($($gameItem.platform)) - Memory: $([math]::Round($gameItem.memoryUsage / 1MB, 0))MB"
}
# Get top processes by CPU usage
$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/top-processes?count=5"
Write-Host "Top 5 processes by CPU usage:"
foreach ($proc in $processes.value) {
Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F2'))% CPU, $($proc.memoryUsagePercentage.ToString('F2'))% Memory"
}
# Terminate a process (example - be careful!)
$killRequest = @{
ProcessId = 1234
Force = $false
} | ConvertTo-Json
# Invoke-RestMethod -Uri "http://localhost:5000/api/process/kill" -Method Post -Body $killRequest -ContentType "application/json"
# Shutdown system with 60-second delay
$shutdownRequest = @{
Action = "shutdown"
DelaySeconds = 60
Message = "Scheduled maintenance shutdown"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5000/api/system/shutdown" -Method Post -Body $shutdownRequest -ContentType "application/json"
```
## 🚨 Known Warnings (Non-Critical)
The service may show warnings in VM environments that don't affect functionality:
- **Performance Counter Warnings**: Some performance counters may not be available in VMs
- **Temperature Sensor Access**: Some temperature sensors require elevated privileges
- **Process Access Denied**: Some system processes require elevated privileges to access
- **Windows.Forms Compatibility**: Game detection works despite .NET Framework compatibility warnings
These warnings are expected in VM environments and the service continues to function normally.
## 🎯 Perfect for Unraid
This service is specifically optimized for Windows VMs running on Unraid:
- **VM Detection**: Automatically detects and reports virtualization status
- **Resource Monitoring**: Tracks VM resource allocation and usage
- **Gaming Support**: Detects games and monitors performance impact
- **Remote Management**: Full API control for integration with Unraid dashboard
- **Alert System**: Configurable alerts for resource thresholds
- **Health Monitoring**: Comprehensive health checks for VM status
## 📝 Logging
The service uses Serilog for structured logging:
- Console output for real-time monitoring
- File logging for persistent records
- Configurable log levels (Debug, Information, Warning, Error)
- Smart error suppression to prevent log spam in VM environments
## 🔐 Security
- Optional API key authentication
- CORS support for web dashboard integration
- Process termination requires explicit API calls
- System shutdown/restart requires explicit API calls
- Configurable allowed origins for API access
## 📈 Performance
- Lightweight background monitoring (5-second intervals by default)
- Efficient memory usage with smart caching and cleanup of old process data
- Non-blocking async operations
- Real-time CPU usage calculation for individual processes
- Graceful error handling for VM-specific limitations
- Configurable monitoring intervals and features
- Smart process tracking with automatic cleanup to prevent memory leaks
## 🆘 Support
For issues or questions:
1. Check the console output for warnings/errors
2. Review the configuration in `appsettings.json`
3. Test individual API endpoints using PowerShell or curl
4. Check Windows Event Logs if running as a service
---
**Version**: 2.1.0
**Target Framework**: .NET 9.0
**Platforms**: Windows (VM optimized)
**License**: Open Source
### Recent Updates
- **v2.1.0**: Added configurable game root folders for enhanced standalone game detection
- **v2.0.0**: Initial release with comprehensive system monitoring and game detection
+6 -1
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,5 +14,9 @@
<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="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+329
View File
@@ -0,0 +1,329 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ResourceMonitorService.Configuration;
using ResourceMonitorService.Models;
using System.Collections.Concurrent;
namespace ResourceMonitorService.Services
{
public interface IAlertService
{
Task CheckAndGenerateAlertsAsync(ResourceUsage resourceUsage);
Task<List<Alert>> GetActiveAlertsAsync();
Task<List<Alert>> GetAlertHistoryAsync(int count = 100);
Task ResolveAlertAsync(string alertId);
Task<bool> IsAlertingEnabledAsync();
event EventHandler<Alert>? AlertTriggered;
event EventHandler<Alert>? AlertResolved;
}
public class AlertService : IAlertService
{
private readonly ILogger<AlertService> _logger;
private readonly MonitoringSettings _settings;
private readonly ITelegramNotificationService _telegramService;
private readonly ConcurrentDictionary<string, Alert> _activeAlerts;
private readonly ConcurrentQueue<Alert> _alertHistory;
private readonly Dictionary<string, DateTime> _lastAlertTime;
private readonly Dictionary<string, DateTime> _thresholdExceededTime;
public event EventHandler<Alert>? AlertTriggered;
public event EventHandler<Alert>? AlertResolved;
public AlertService(ILogger<AlertService> logger, IOptions<MonitoringSettings> settings, ITelegramNotificationService telegramService)
{
_logger = logger;
_settings = settings.Value;
_telegramService = telegramService;
_activeAlerts = new ConcurrentDictionary<string, Alert>();
_alertHistory = new ConcurrentQueue<Alert>();
_lastAlertTime = new Dictionary<string, DateTime>();
_thresholdExceededTime = new Dictionary<string, DateTime>();
}
public async Task CheckAndGenerateAlertsAsync(ResourceUsage resourceUsage)
{
if (!_settings.EnableAlerts)
return;
try
{
await Task.Run(() =>
{
// Check CPU usage
CheckThreshold("CPU", resourceUsage.CPU.Usage, "CPU Usage", "%");
// Check CPU temperature
if (resourceUsage.CPU.Temperature > 0)
CheckThreshold("CPUTemp", resourceUsage.CPU.Temperature, "CPU Temperature", "°C");
// Check Memory usage
CheckThreshold("Memory", resourceUsage.Memory.UsagePercentage, "Memory Usage", "%");
// Check GPU usage
if (resourceUsage.GPU.IsAvailable)
{
CheckThreshold("GPU", resourceUsage.GPU.Usage, "GPU Usage", "%");
if (resourceUsage.GPU.Temperature > 0)
CheckThreshold("GPUTemp", resourceUsage.GPU.Temperature, "GPU Temperature", "°C");
}
// Check disk usage
foreach (var disk in resourceUsage.Disks)
{
CheckThreshold($"Disk_{disk.DriveLetter}", disk.UsagePercentage,
$"Disk Usage ({disk.DriveLetter})", "%");
if (disk.DiskTime > 0)
CheckThreshold($"DiskTime_{disk.DriveLetter}", disk.DiskTime,
$"Disk Time ({disk.DriveLetter})", "%");
}
// Check for processes using too much memory
var topMemoryProcess = resourceUsage.TopProcesses
.OrderByDescending(p => p.MemoryUsage)
.FirstOrDefault();
if (topMemoryProcess != null)
{
var memoryUsageGB = topMemoryProcess.MemoryUsage / (1024.0 * 1024.0 * 1024.0);
if (memoryUsageGB > 4) // Alert if a single process is using more than 4GB
{
CheckCustomAlert($"ProcessMemory_{topMemoryProcess.Name}",
(float)memoryUsageGB, 4f, 8f,
$"High Memory Usage - {topMemoryProcess.Name}", "GB");
}
}
// Resolve alerts that are no longer active
ResolveInactiveAlerts(resourceUsage);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking and generating alerts");
}
}
private void CheckThreshold(string component, float currentValue, string description, string unit)
{
var threshold = _settings.AlertThresholds.FirstOrDefault(t =>
t.Component.Equals(component, StringComparison.OrdinalIgnoreCase));
if (threshold == null || !threshold.IsEnabled)
return;
CheckCustomAlert(component, currentValue, threshold.WarningThreshold,
threshold.CriticalThreshold, description, unit, TimeSpan.FromSeconds(threshold.DurationSeconds));
}
private void CheckCustomAlert(string component, float currentValue, float warningThreshold,
float criticalThreshold, string description, string unit, TimeSpan? duration = null)
{
var alertDuration = duration ?? TimeSpan.FromSeconds(30);
var now = DateTime.Now;
// Determine alert level
string? alertLevel = null;
float thresholdValue = 0;
if (currentValue >= criticalThreshold)
{
alertLevel = "Critical";
thresholdValue = criticalThreshold;
}
else if (currentValue >= warningThreshold)
{
alertLevel = "Warning";
thresholdValue = warningThreshold;
}
if (alertLevel != null)
{
// Check if threshold has been exceeded for the required duration
var key = $"{component}_{alertLevel}";
if (!_thresholdExceededTime.ContainsKey(key))
{
_thresholdExceededTime[key] = now;
return; // Not exceeded long enough yet
}
var exceededDuration = now - _thresholdExceededTime[key];
if (exceededDuration < alertDuration)
return; // Not exceeded long enough yet
// Check if we've already sent this alert recently (avoid spam)
if (_lastAlertTime.TryGetValue(key, out var lastAlert))
{
if (now - lastAlert < TimeSpan.FromMinutes(5))
return; // Too soon since last alert
}
// Create and trigger alert
var alert = new Alert
{
Timestamp = now,
Component = component,
Level = alertLevel,
Message = $"{description} is {alertLevel.ToLower()}: {currentValue:F1}{unit} (threshold: {thresholdValue:F1}{unit})",
CurrentValue = currentValue,
ThresholdValue = thresholdValue,
IsResolved = false
};
var alertId = $"{component}_{alertLevel}_{now:yyyyMMddHHmmss}";
_activeAlerts[alertId] = alert;
_alertHistory.Enqueue(alert);
_lastAlertTime[key] = now;
// Trim history if too large
while (_alertHistory.Count > 1000)
{
_alertHistory.TryDequeue(out _);
}
_logger.LogWarning("Alert triggered: {Message}", alert.Message);
AlertTriggered?.Invoke(this, alert);
// Send Telegram notification
_ = Task.Run(async () =>
{
try
{
await _telegramService.SendAlertAsync(alert);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send Telegram alert notification");
}
});
}
else
{
// Value is below threshold, remove tracking
var warningKey = $"{component}_Warning";
var criticalKey = $"{component}_Critical";
_thresholdExceededTime.Remove(warningKey);
_thresholdExceededTime.Remove(criticalKey);
}
}
private void ResolveInactiveAlerts(ResourceUsage resourceUsage)
{
var now = DateTime.Now;
var alertsToResolve = new List<string>();
foreach (var activeAlert in _activeAlerts)
{
var alert = activeAlert.Value;
var shouldResolve = false;
// Check if the condition that triggered the alert is no longer true
switch (alert.Component)
{
case "CPU":
shouldResolve = resourceUsage.CPU.Usage < alert.ThresholdValue;
break;
case "CPUTemp":
shouldResolve = resourceUsage.CPU.Temperature < alert.ThresholdValue;
break;
case "Memory":
shouldResolve = resourceUsage.Memory.UsagePercentage < alert.ThresholdValue;
break;
case "GPU":
shouldResolve = !resourceUsage.GPU.IsAvailable || resourceUsage.GPU.Usage < alert.ThresholdValue;
break;
case "GPUTemp":
shouldResolve = !resourceUsage.GPU.IsAvailable || resourceUsage.GPU.Temperature < alert.ThresholdValue;
break;
default:
// For disk alerts and others, check if component still exists and is below threshold
if (alert.Component.StartsWith("Disk_"))
{
var driveLetter = alert.Component.Replace("Disk_", "").Replace("DiskTime_", "");
var disk = resourceUsage.Disks.FirstOrDefault(d => d.DriveLetter.Contains(driveLetter));
if (disk != null)
{
shouldResolve = alert.Component.StartsWith("DiskTime_")
? disk.DiskTime < alert.ThresholdValue
: disk.UsagePercentage < alert.ThresholdValue;
}
else
{
shouldResolve = true; // Disk no longer available
}
}
break;
}
// Auto-resolve old alerts (older than 1 hour)
if (now - alert.Timestamp > TimeSpan.FromHours(1))
{
shouldResolve = true;
}
if (shouldResolve)
{
alertsToResolve.Add(activeAlert.Key);
}
}
// Resolve alerts
foreach (var alertId in alertsToResolve)
{
if (_activeAlerts.TryRemove(alertId, out var resolvedAlert))
{
resolvedAlert.IsResolved = true;
resolvedAlert.ResolvedAt = now;
_logger.LogInformation("Alert resolved: {Message}", resolvedAlert.Message);
AlertResolved?.Invoke(this, resolvedAlert);
// Send Telegram resolution notification
_ = Task.Run(async () =>
{
try
{
await _telegramService.SendAlertResolvedAsync(resolvedAlert);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send Telegram resolution notification");
}
});
}
}
}
public async Task<List<Alert>> GetActiveAlertsAsync()
{
return await Task.FromResult(_activeAlerts.Values.ToList());
}
public async Task<List<Alert>> GetAlertHistoryAsync(int count = 100)
{
return await Task.FromResult(_alertHistory.TakeLast(count).ToList());
}
public async Task ResolveAlertAsync(string alertId)
{
await Task.Run(() =>
{
if (_activeAlerts.TryRemove(alertId, out var alert))
{
alert.IsResolved = true;
alert.ResolvedAt = DateTime.Now;
_logger.LogInformation("Alert manually resolved: {Message}", alert.Message);
AlertResolved?.Invoke(this, alert);
}
});
}
public async Task<bool> IsAlertingEnabledAsync()
{
return await Task.FromResult(_settings.EnableAlerts);
}
}
}
+524
View File
@@ -0,0 +1,524 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ResourceMonitorService.Configuration;
using ResourceMonitorService.Models;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace ResourceMonitorService.Services
{
public interface IGameDetectionService
{
Task<GameInfo?> GetCurrentlyRunningGameAsync();
Task<List<GameInfo>> GetAllDetectedGamesAsync();
Task<bool> IsGameRunningFullscreenAsync();
Task<float> GetGameFpsAsync(string processName);
}
public class GameDetectionService : IGameDetectionService
{
private readonly ILogger<GameDetectionService> _logger;
private readonly MonitoringSettings _settings;
// Windows API imports for fullscreen detection
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public GameDetectionService(ILogger<GameDetectionService> logger, IOptions<MonitoringSettings> settings)
{
_logger = logger;
_settings = settings.Value;
}
public async Task<GameInfo?> GetCurrentlyRunningGameAsync()
{
try
{
return await Task.Run(() =>
{
var processes = Process.GetProcesses();
foreach (var process in processes)
{
try
{
if (process.MainModule?.FileName == null)
continue;
var filePath = process.MainModule.FileName;
var gameInfo = DetectGameFromPath(filePath, process);
if (gameInfo != null)
{
gameInfo.IsFullscreen = IsGameRunningFullscreenAsync().Result;
gameInfo.FPS = GetGameFpsAsync(process.ProcessName).Result;
return gameInfo;
}
}
catch (Exception ex)
{
// Handle access exceptions silently - some processes can't be accessed
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
}
}
return null;
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting currently running game");
return null;
}
}
public async Task<List<GameInfo>> GetAllDetectedGamesAsync()
{
try
{
return await Task.Run(() =>
{
var games = new List<GameInfo>();
var processes = Process.GetProcesses();
foreach (var process in processes)
{
try
{
if (process.MainModule?.FileName == null)
continue;
var filePath = process.MainModule.FileName;
var gameInfo = DetectGameFromPath(filePath, process);
if (gameInfo != null)
{
games.Add(gameInfo);
}
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
}
}
return games;
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting all detected games");
return new List<GameInfo>();
}
}
public async Task<bool> IsGameRunningFullscreenAsync()
{
try
{
return await Task.Run(() =>
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == IntPtr.Zero)
return false;
if (!IsWindowVisible(foregroundWindow))
return false;
if (!GetWindowRect(foregroundWindow, out RECT rect))
return false;
// Get screen dimensions
var primaryScreen = System.Windows.Forms.Screen.PrimaryScreen;
if (primaryScreen == null)
return false;
var screenWidth = primaryScreen.Bounds.Width;
var screenHeight = primaryScreen.Bounds.Height;
// Check if window covers the entire screen
var windowWidth = rect.Right - rect.Left;
var windowHeight = rect.Bottom - rect.Top;
return windowWidth >= screenWidth && windowHeight >= screenHeight;
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not determine if game is running fullscreen");
return false;
}
}
public async Task<float> GetGameFpsAsync(string processName)
{
try
{
return await Task.Run(() =>
{
// This is a simplified FPS detection - in reality, you'd need more sophisticated methods
// such as hooking into DirectX/OpenGL or using external tools like RTSS
// For now, we'll return 0 as a placeholder
// In a real implementation, you might:
// 1. Use Windows Performance Toolkit (WPT) ETW events
// 2. Hook into D3D11/D3D12 present calls
// 3. Use NVIDIA's NVAPI or AMD's ADL
// 4. Parse log files from games that output FPS
return 0f;
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not get FPS for process {ProcessName}", processName);
return 0f;
}
}
private GameInfo? DetectGameFromPath(string filePath, Process process)
{
try
{
// First check configured game root folders
var gameFromRootFolder = DetectGameFromRootFolders(filePath, process);
if (gameFromRootFolder != null)
{
return gameFromRootFolder;
}
// Check each configured game platform path
foreach (var platformPath in _settings.GamePlatformPaths)
{
if (filePath.Contains(platformPath, StringComparison.OrdinalIgnoreCase))
{
var platform = GetPlatformFromPath(platformPath);
var gameName = ExtractGameNameFromPath(filePath, platformPath);
return new GameInfo
{
GameName = gameName,
ExecutableName = Path.GetFileName(filePath),
FullPath = filePath,
ProcessId = process.Id,
MemoryUsage = (ulong)process.WorkingSet64,
CpuTime = process.TotalProcessorTime,
StartTime = process.StartTime,
Platform = platform,
IsFullscreen = false, // Will be set by caller
FPS = 0f // Will be set by caller
};
}
}
// Additional checks for common game launchers and executables
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
// Exclude known system processes and applications
var systemExclusions = new[]
{
"officeclicktorun", "winword", "excel", "powerpoint", "outlook",
"teams", "skype", "chrome", "firefox", "edge", "explorer",
"notepad", "calculator", "cmd", "powershell", "taskmgr",
"svchost", "dwm", "csrss", "winlogon", "lsass", "services",
"wininit", "audiodg", "conhost", "rundll32", "msiexec",
"setup", "installer", "update", "vshost", "devenv"
};
// Skip if it's a known system process
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)))
{
return null;
}
var knownGameExecutables = new[]
{
"game", "launcher", "client"
// Removed generic terms like "main", "start", "run" that match too many system processes
};
var gameIndicators = new[]
{
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
"steam", "epic", "origin", "uplay", "battle.net"
};
// Check if it's likely a game based on executable name or path
// Made the condition more restrictive to reduce false positives
if ((knownGameExecutables.Any(exe => fileName.Equals(exe) || fileName.StartsWith(exe + ".")) ||
gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase))) &&
!filePath.Contains("Program Files\\Common Files", StringComparison.OrdinalIgnoreCase))
{
// Try to determine platform and game name from other indicators
var platform = DeterminePlatformFromProcess(process, filePath);
var gameName = DetermineGameNameFromProcess(process, filePath);
if (!string.IsNullOrEmpty(gameName))
{
return new GameInfo
{
GameName = gameName,
ExecutableName = Path.GetFileName(filePath),
FullPath = filePath,
ProcessId = process.Id,
MemoryUsage = (ulong)process.WorkingSet64,
CpuTime = process.TotalProcessorTime,
StartTime = process.StartTime,
Platform = platform,
IsFullscreen = false,
FPS = 0f
};
}
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error detecting game from path {FilePath}", filePath);
return null;
}
}
private string GetPlatformFromPath(string platformPath)
{
return platformPath.ToLowerInvariant() switch
{
var path when path.Contains("steamapps") => "Steam",
var path when path.Contains("epic games") => "Epic Games Store",
var path when path.Contains("gog galaxy") => "GOG Galaxy",
var path when path.Contains("origin games") => "EA Origin",
var path when path.Contains("ubisoft game launcher") => "Ubisoft Connect",
_ => "Unknown"
};
}
private string ExtractGameNameFromPath(string filePath, string platformPath)
{
try
{
var parts = filePath.Split(new[] { platformPath }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1)
{
var gamePath = parts[1];
var gameFolder = gamePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[0];
return gameFolder;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not extract game name from path {FilePath}", filePath);
}
return Path.GetFileNameWithoutExtension(filePath);
}
private string DeterminePlatformFromProcess(Process process, string filePath)
{
try
{
// Check parent processes for launcher indicators
var currentProcess = process;
for (int i = 0; i < 3; i++) // Check up to 3 levels up
{
try
{
var parentId = GetParentProcessId(currentProcess.Id);
if (parentId == 0) break;
var parentProcess = Process.GetProcessById(parentId);
var parentName = parentProcess.ProcessName.ToLowerInvariant();
if (parentName.Contains("steam"))
return "Steam";
if (parentName.Contains("epic"))
return "Epic Games Store";
if (parentName.Contains("origin"))
return "EA Origin";
if (parentName.Contains("uplay") || parentName.Contains("ubisoft"))
return "Ubisoft Connect";
if (parentName.Contains("gog"))
return "GOG Galaxy";
currentProcess = parentProcess;
}
catch
{
break;
}
}
// Fallback: check file path for platform indicators
if (filePath.Contains("Program Files (x86)"))
return "Windows Store/Other";
if (filePath.Contains("WindowsApps"))
return "Microsoft Store";
return "Standalone";
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not determine platform for process {ProcessName}", process.ProcessName);
return "Unknown";
}
}
private string DetermineGameNameFromProcess(Process process, string filePath)
{
try
{
// Try to get a meaningful name from various sources
// 1. Try from file properties
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
{
return versionInfo.ProductName;
}
// 2. Try from directory name
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
var directoryName = Path.GetFileName(directory);
if (!string.IsNullOrEmpty(directoryName) &&
!directoryName.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
!directoryName.Equals("exe", StringComparison.OrdinalIgnoreCase))
{
return directoryName;
}
}
// 3. Fallback to executable name
return Path.GetFileNameWithoutExtension(filePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not determine game name for process {ProcessName}", process.ProcessName);
return process.ProcessName;
}
}
private GameInfo? DetectGameFromRootFolders(string filePath, Process process)
{
try
{
foreach (var rootFolder in _settings.GameRootFolders)
{
if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase))
{
var gameName = ExtractGameNameFromRootFolder(filePath, rootFolder);
return new GameInfo
{
GameName = gameName,
ExecutableName = Path.GetFileName(filePath),
FullPath = filePath,
ProcessId = process.Id,
MemoryUsage = (ulong)process.WorkingSet64,
CpuTime = process.TotalProcessorTime,
StartTime = process.StartTime,
Platform = "Standalone", // Games in root folders are typically standalone
IsFullscreen = false, // Will be set by caller
FPS = 0f // Will be set by caller
};
}
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error detecting game from root folders for path {FilePath}", filePath);
return null;
}
}
private string ExtractGameNameFromRootFolder(string filePath, string rootFolder)
{
try
{
// Remove the root folder from the path to get the relative game path
var relativePath = filePath.Substring(rootFolder.Length).TrimStart('\\', '/');
// Split by directory separator and take the first part as the game folder
var pathParts = relativePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length > 0)
{
var gameFolder = pathParts[0];
// If the game folder name is reasonable, use it
if (!string.IsNullOrEmpty(gameFolder) &&
!gameFolder.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
!gameFolder.Equals("exe", StringComparison.OrdinalIgnoreCase) &&
!gameFolder.Equals("data", StringComparison.OrdinalIgnoreCase))
{
return gameFolder;
}
}
// Fallback: try to get the game name from file properties
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
{
return versionInfo.ProductName;
}
// Last resort: use the executable name
return Path.GetFileNameWithoutExtension(filePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not extract game name from root folder path {FilePath}", filePath);
return Path.GetFileNameWithoutExtension(filePath);
}
}
private int GetParentProcessId(int processId)
{
try
{
using var searcher = new System.Management.ManagementObjectSearcher(
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}");
using var collection = searcher.Get();
foreach (System.Management.ManagementObject obj in collection)
{
return Convert.ToInt32(obj["ParentProcessId"]);
}
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Could not get parent process ID for {ProcessId}", processId);
}
return 0;
}
}
}
File diff suppressed because it is too large Load Diff
+217
View File
@@ -0,0 +1,217 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ResourceMonitorService.Configuration;
using ResourceMonitorService.Models;
using System.Diagnostics;
using System.Management;
namespace ResourceMonitorService.Services
{
public interface ISystemInfoService
{
Task<SystemInfo> GetSystemInfoAsync();
Task<bool> IsVirtualMachineAsync();
Task<string> GetHypervisorVendorAsync();
Task<DateTime> GetBootTimeAsync();
Task<string> GetCpuNameAsync();
}
public class SystemInfoService : ISystemInfoService
{
private readonly ILogger<SystemInfoService> _logger;
private readonly MonitoringSettings _settings;
private SystemInfo? _cachedSystemInfo;
private DateTime _lastCacheUpdate = DateTime.MinValue;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5);
public SystemInfoService(ILogger<SystemInfoService> logger, IOptions<MonitoringSettings> settings)
{
_logger = logger;
_settings = settings.Value;
}
public async Task<SystemInfo> GetSystemInfoAsync()
{
if (_cachedSystemInfo != null && DateTime.Now - _lastCacheUpdate < _cacheExpiration)
{
_cachedSystemInfo.Uptime = DateTime.Now - _cachedSystemInfo.BootTime;
return _cachedSystemInfo;
}
try
{
var systemInfo = new SystemInfo
{
MachineName = Environment.MachineName,
OSVersion = System.Runtime.InteropServices.RuntimeInformation.OSDescription,
OSArchitecture = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture.ToString(),
ProcessorCount = Environment.ProcessorCount,
TotalPhysicalMemory = await GetTotalPhysicalMemoryAsync(),
CPUName = await GetCpuNameAsync(),
BootTime = await GetBootTimeAsync(),
Domain = Environment.UserDomainName,
IsVirtualMachine = await IsVirtualMachineAsync(),
HypervisorVendor = await GetHypervisorVendorAsync()
};
systemInfo.Uptime = DateTime.Now - systemInfo.BootTime;
_cachedSystemInfo = systemInfo;
_lastCacheUpdate = DateTime.Now;
return systemInfo;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting system information");
throw;
}
}
public async Task<bool> IsVirtualMachineAsync()
{
try
{
return await Task.Run(() =>
{
// Check for common VM indicators
var queries = new[]
{
"SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE '%VMware%' OR Manufacturer LIKE '%VirtualBox%' OR Manufacturer LIKE '%Microsoft Corporation%' OR Model LIKE '%Virtual%'",
"SELECT * FROM Win32_BIOS WHERE SerialNumber LIKE '%VMware%' OR SerialNumber LIKE '%VirtualBox%' OR Version LIKE '%VBOX%'",
"SELECT * FROM Win32_SystemEnclosure WHERE Manufacturer LIKE '%VMware%' OR Manufacturer LIKE '%VirtualBox%'"
};
foreach (var query in queries)
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher(query);
using var collection = searcher.Get();
if (collection.Count > 0)
return true;
#pragma warning restore CA1416 // Validate platform compatibility
}
return false;
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not determine if running in virtual machine");
return false;
}
}
public async Task<string> GetHypervisorVendorAsync()
{
try
{
return await Task.Run(() =>
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
var manufacturer = obj["Manufacturer"]?.ToString() ?? "";
var model = obj["Model"]?.ToString() ?? "";
if (manufacturer.Contains("VMware"))
return "VMware";
if (manufacturer.Contains("Microsoft Corporation") && model.Contains("Virtual"))
return "Hyper-V";
if (manufacturer.Contains("QEMU"))
return "QEMU/KVM";
if (manufacturer.Contains("VirtualBox"))
return "VirtualBox";
if (manufacturer.Contains("Xen"))
return "Xen";
}
#pragma warning restore CA1416 // Validate platform compatibility
return "Unknown";
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not determine hypervisor vendor");
return "Unknown";
}
}
public async Task<DateTime> GetBootTimeAsync()
{
try
{
return await Task.Run(() =>
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher("SELECT LastBootUpTime FROM Win32_OperatingSystem");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
var bootTime = obj["LastBootUpTime"]?.ToString();
if (!string.IsNullOrEmpty(bootTime))
{
return ManagementDateTimeConverter.ToDateTime(bootTime);
}
}
#pragma warning restore CA1416 // Validate platform compatibility
return DateTime.Now.AddMilliseconds(-Environment.TickCount64);
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not get boot time from WMI, using tick count");
return DateTime.Now.AddMilliseconds(-Environment.TickCount64);
}
}
public async Task<string> GetCpuNameAsync()
{
try
{
return await Task.Run(() =>
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_Processor");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
return obj["Name"]?.ToString()?.Trim() ?? "Unknown CPU";
}
#pragma warning restore CA1416 // Validate platform compatibility
return "Unknown CPU";
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not get CPU name");
return "Unknown CPU";
}
}
private async Task<ulong> GetTotalPhysicalMemoryAsync()
{
try
{
return await Task.Run(() =>
{
#pragma warning disable CA1416 // Validate platform compatibility
using var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
return (ulong)obj["TotalPhysicalMemory"];
}
#pragma warning restore CA1416 // Validate platform compatibility
return (ulong)0;
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not get total physical memory");
return 0;
}
}
}
}
+206
View File
@@ -0,0 +1,206 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ResourceMonitorService.Configuration;
using ResourceMonitorService.Models;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Types.Enums;
namespace ResourceMonitorService.Services
{
public interface ITelegramNotificationService
{
Task SendAlertAsync(Alert alert);
Task SendAlertResolvedAsync(Alert alert);
Task<bool> IsEnabledAsync();
Task<bool> TestConnectionAsync();
}
public class TelegramNotificationService : ITelegramNotificationService
{
private readonly ILogger<TelegramNotificationService> _logger;
private readonly TelegramSettings _telegramSettings;
private readonly ITelegramBotClient? _botClient;
public TelegramNotificationService(
ILogger<TelegramNotificationService> logger,
IOptions<MonitoringSettings> settings)
{
_logger = logger;
_telegramSettings = settings.Value.Telegram;
if (_telegramSettings.IsEnabled && !string.IsNullOrEmpty(_telegramSettings.BotToken))
{
try
{
_botClient = new TelegramBotClient(_telegramSettings.BotToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize Telegram bot client");
}
}
}
public async Task<bool> IsEnabledAsync()
{
return await Task.FromResult(_telegramSettings.IsEnabled && _botClient != null);
}
public async Task<bool> TestConnectionAsync()
{
if (_botClient == null || !_telegramSettings.IsEnabled)
return false;
try
{
var me = await _botClient.GetMe();
_logger.LogInformation("Telegram bot connected successfully: @{Username}", me.Username);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to Telegram bot");
return false;
}
}
public async Task SendAlertAsync(Alert alert)
{
if (_botClient == null || !_telegramSettings.IsEnabled)
return;
// Check if we should send this type of alert
if ((alert.Level == "Warning" && !_telegramSettings.SendWarningAlerts) ||
(alert.Level == "Critical" && !_telegramSettings.SendCriticalAlerts))
{
return;
}
var message = FormatAlertMessage(alert, _telegramSettings.MessageTemplate);
foreach (var chatId in _telegramSettings.ChatIds)
{
try
{
await _botClient.SendMessage(
chatId: chatId,
text: message,
parseMode: ParseMode.Markdown,
disableNotification: alert.Level == "Warning" // Don't ping for warnings
);
_logger.LogInformation("Telegram alert sent to chat {ChatId}: {AlertLevel} - {Component}",
chatId, alert.Level, alert.Component);
}
catch (ApiRequestException ex)
{
_logger.LogError(ex, "Failed to send Telegram alert to chat {ChatId}: {ErrorCode} - {Description}",
chatId, ex.ErrorCode, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error sending Telegram alert to chat {ChatId}", chatId);
}
}
}
public async Task SendAlertResolvedAsync(Alert alert)
{
if (_botClient == null || !_telegramSettings.IsEnabled || !_telegramSettings.SendResolutionNotifications)
return;
var message = FormatAlertMessage(alert, _telegramSettings.ResolutionTemplate);
foreach (var chatId in _telegramSettings.ChatIds)
{
try
{
await _botClient.SendMessage(
chatId: chatId,
text: message,
parseMode: ParseMode.Markdown,
disableNotification: true // Don't ping for resolutions
);
_logger.LogInformation("Telegram resolution notification sent to chat {ChatId}: {Component}",
chatId, alert.Component);
}
catch (ApiRequestException ex)
{
_logger.LogError(ex, "Failed to send Telegram resolution to chat {ChatId}: {ErrorCode} - {Description}",
chatId, ex.ErrorCode, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error sending Telegram resolution to chat {ChatId}", chatId);
}
}
}
private string FormatAlertMessage(Alert alert, string template)
{
var levelIcon = alert.Level switch
{
"Critical" => "🔴",
"Warning" => "⚠️",
_ => "️"
};
var componentIcon = alert.Component switch
{
"CPU" => "🖥️",
"CPUTemp" => "🌡️",
"Memory" => "💾",
"GPU" => "🎮",
"GPUTemp" => "🌡️",
var disk when disk.StartsWith("Disk") => "💽",
var process when process.StartsWith("ProcessMemory") => "⚙️",
_ => "📊"
};
// Replace template placeholders
var message = template
.Replace("{Level}", alert.Level)
.Replace("{Component}", alert.Component)
.Replace("{Message}", EscapeMarkdown(alert.Message))
.Replace("{Timestamp}", alert.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{CurrentValue}", alert.CurrentValue.ToString("F1"))
.Replace("{ThresholdValue}", alert.ThresholdValue.ToString("F1"));
if (alert.ResolvedAt.HasValue)
{
message = message.Replace("{ResolvedAt}", alert.ResolvedAt.Value.ToString("yyyy-MM-dd HH:mm:ss"));
}
// Add icons
message = $"{levelIcon} {componentIcon} {message}";
return message;
}
private static string EscapeMarkdown(string text)
{
// Escape special Markdown characters
return text
.Replace("_", "\\_")
.Replace("*", "\\*")
.Replace("[", "\\[")
.Replace("]", "\\]")
.Replace("(", "\\(")
.Replace(")", "\\)")
.Replace("~", "\\~")
.Replace("`", "\\`")
.Replace(">", "\\>")
.Replace("#", "\\#")
.Replace("+", "\\+")
.Replace("-", "\\-")
.Replace("=", "\\=")
.Replace("|", "\\|")
.Replace("{", "\\{")
.Replace("}", "\\}")
.Replace(".", "\\.")
.Replace("!", "\\!");
}
}
}
+196
View File
@@ -0,0 +1,196 @@
# Telegram Bot Alert Setup Guide
The Resource Monitor Service supports sending alerts via Telegram bot. This allows you to receive real-time notifications about system resource warnings and critical alerts directly to your Telegram chat.
## Prerequisites
1. **Create a Telegram Bot**:
- Open Telegram and search for `@BotFather`
- Send `/newbot` command
- Follow the instructions to create a new bot
- Save the Bot Token (format: `123456789:ABCdefGHIjklMNOpqrSTUvwxyz`)
2. **Get Your Chat ID**:
- Send a message to your bot
- Visit: `https://api.telegram.org/bot<YourBOTToken>/getUpdates`
- Find your chat ID in the response (it's a number, can be negative for groups)
## Configuration
### 1. Edit appsettings.json
Add or update the Telegram configuration in your `appsettings.json`:
```json
{
"MonitoringSettings": {
// ... other settings ...
"Telegram": {
"IsEnabled": true,
"BotToken": "123456789:ABCdefGHIjklMNOpqrSTUvwxyz",
"ChatIds": [12345678, -987654321],
"SendWarningAlerts": true,
"SendCriticalAlerts": true,
"SendResolutionNotifications": true,
"MessageTemplate": "🚨 *{Level} Alert*\n\n📊 *{Component}*\n💬 {Message}\n⏰ {Timestamp}",
"ResolutionTemplate": "✅ *Alert Resolved*\n\n📊 *{Component}*\n💬 {Message}\n⏰ Resolved at {ResolvedAt}"
}
}
}
```
### 2. Configuration Options
| Setting | Description | Default |
|---------|-------------|---------|
| `IsEnabled` | Enable/disable Telegram notifications | `false` |
| `BotToken` | Your Telegram bot token from BotFather | `""` |
| `ChatIds` | Array of chat IDs to send alerts to | `[]` |
| `SendWarningAlerts` | Send warning level alerts | `true` |
| `SendCriticalAlerts` | Send critical level alerts | `true` |
| `SendResolutionNotifications` | Send alert resolution notifications | `true` |
| `MessageTemplate` | Template for alert messages | See above |
| `ResolutionTemplate` | Template for resolution messages | See above |
### 3. Message Templates
Templates support the following placeholders:
- `{Level}` - Alert level (Warning, Critical)
- `{Component}` - Component name (CPU, Memory, GPU, etc.)
- `{Message}` - Full alert message
- `{Timestamp}` - Alert timestamp
- `{CurrentValue}` - Current resource value
- `{ThresholdValue}` - Threshold that was exceeded
- `{ResolvedAt}` - Resolution timestamp (resolution template only)
## API Endpoints
### Check Telegram Status
```
GET /api/telegram/status
```
Returns the current status of Telegram integration:
```json
{
"enabled": true,
"connected": true
}
```
### Send Test Alert
```
POST /api/telegram/test
```
Sends a test alert to verify the Telegram bot is working correctly.
## Features
### Alert Types
The bot sends different types of alerts:
1. **Warning Alerts** ⚠️
- Sent when thresholds are exceeded but not critical
- Notifications are silent (no sound/vibration)
2. **Critical Alerts** 🔴
- Sent when critical thresholds are exceeded
- Normal notifications (with sound/vibration)
3. **Resolution Notifications**
- Sent when alerts are resolved
- Always silent notifications
### Icons and Formatting
The bot automatically adds relevant icons:
- 🖥️ CPU usage
- 🌡️ Temperature alerts
- 💾 Memory usage
- 🎮 GPU usage
- 💽 Disk usage
- ⚙️ Process alerts
Messages are formatted using Telegram's Markdown formatting for better readability.
### Multiple Chat Support
You can send alerts to multiple chats:
- Personal chats
- Group chats
- Channels (if bot is admin)
Just add multiple chat IDs to the `ChatIds` array.
## Troubleshooting
### Common Issues
1. **Bot not responding**:
- Verify bot token is correct
- Ensure bot is not blocked
- Check `/api/telegram/status` endpoint
2. **Messages not received**:
- Verify chat ID is correct
- Ensure you've sent at least one message to the bot
- Check bot has permission to send messages
3. **Connection errors**:
- Check internet connectivity
- Verify Telegram API is accessible
- Check firewall settings
### Testing
1. Set `IsEnabled: true` in configuration
2. Restart the service
3. Call `POST /api/telegram/test` to send a test message
4. Check if the message is received in Telegram
### Logs
Monitor the service logs for Telegram-related errors:
```
grep -i telegram logs/resourcemonitor-*.txt
```
## Security Considerations
1. **Keep bot token secure** - Never commit it to version control
2. **Use environment variables** for sensitive configuration in production
3. **Limit chat IDs** to trusted users/groups only
4. **Regular token rotation** if compromised
## Example Alert Messages
### Warning Alert
```
⚠️ 🖥️ 🚨 Warning Alert
📊 CPU
💬 CPU Usage is warning: 85.2% (threshold: 80.0%)
⏰ 2025-08-07 14:30:15
```
### Critical Alert
```
🔴 💾 🚨 Critical Alert
📊 Memory
💬 Memory Usage is critical: 96.8% (threshold: 95.0%)
⏰ 2025-08-07 14:35:22
```
### Resolution
```
✅ 🖥️ Alert Resolved
📊 CPU
💬 CPU Usage is warning: 85.2% (threshold: 80.0%)
⏰ Resolved at 2025-08-07 14:32:45
```
+426 -353
View File
@@ -1,166 +1,280 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.AspNetCore.Http; using ResourceMonitorService.Configuration;
using System.Runtime.InteropServices; using ResourceMonitorService.Models;
using System.Management; using ResourceMonitorService.Services;
using System.Diagnostics;
namespace ResourceMonitorService namespace ResourceMonitorService
{ {
public class Worker : BackgroundService public class Worker : BackgroundService
{ {
private readonly ILogger<Worker> _logger;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
private readonly ISystemInfoService _systemInfoService;
private readonly IResourceMonitorService _resourceMonitorService;
private readonly IGameDetectionService _gameDetectionService;
private readonly IAlertService _alertService;
private readonly ITelegramNotificationService _telegramService;
private readonly ApiSettings _apiSettings;
private readonly MonitoringSettings _monitoringSettings;
public Worker(IHostApplicationLifetime lifetime) public Worker(
ILogger<Worker> logger,
IHostApplicationLifetime lifetime,
ISystemInfoService systemInfoService,
IResourceMonitorService resourceMonitorService,
IGameDetectionService gameDetectionService,
IAlertService alertService,
ITelegramNotificationService telegramService,
IOptions<ApiSettings> apiSettings,
IOptions<MonitoringSettings> monitoringSettings)
{ {
_logger = logger;
_lifetime = lifetime; _lifetime = lifetime;
_systemInfoService = systemInfoService;
_resourceMonitorService = resourceMonitorService;
_gameDetectionService = gameDetectionService;
_alertService = alertService;
_telegramService = telegramService;
_apiSettings = apiSettings.Value;
_monitoringSettings = monitoringSettings.Value;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
var builder = WebApplication.CreateBuilder(); _logger.LogInformation("Resource Monitor Service starting...");
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins",
builder => builder
.WithOrigins("http://localhost:4200","http://192.168.50.52:4200","http://vmwin11:4200")
.AllowAnyHeader()
.AllowAnyMethod());
});
builder.Services.AddControllers().AddNewtonsoftJson();
// Read the API key from appsettings.json var builder = WebApplication.CreateBuilder();
var configuration = builder.Configuration;
var apiKey = configuration["ApiSettings:ApiKey"]; // 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(); var app = builder.Build();
// Middleware to validate API key // API Key middleware (if enabled)
// This middleware checks for the presence of the API key in the request headers if (_apiSettings.RequireApiKey)
// and compares it with the expected API key from appsettings.json.
// If the API key is missing or invalid, it returns a 401 Unauthorized response.
//
/* app.Use(async (context, next) =>
{
if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) || extractedApiKey != apiKey)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Unauthorized: Invalid API Key");
return;
}
await next();
}); */
// Apply CORS policy to allow all origins
app.UseCors("AllowAllOrigins");
app.MapGet("/api/resource-usage", async context =>
{ {
var currentTime = GetCurrentTime(); app.Use(async (context, next) =>
var computerInfo = GetComputerInfo();
var cpuUsage = GetCpuUsage();
var ramUsage = GetRamUsage();
var gpuUsage = GetGpuUsage();
var runningGame = GetCurrentlyRunningGame();
var resourceUsage = new
{ {
CurrentTime = currentTime, if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) ||
ComputerInfo = computerInfo, extractedApiKey != _apiSettings.ApiKey)
CPU = cpuUsage, {
RAM = ramUsage, context.Response.StatusCode = StatusCodes.Status401Unauthorized;
GPU = gpuUsage, await context.Response.WriteAsync("Unauthorized: Invalid API Key");
CurrentlyRunningGame = runningGame return;
}; }
await next();
});
}
var json = JsonConvert.SerializeObject(resourceUsage); app.UseCors("AllowedOrigins");
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(json); // 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.MapPost("/api/kill-process", async context => app.MapGet($"{basePath}/alerts/enabled", async () =>
{ Results.Ok(new { Enabled = await _alertService.IsAlertingEnabledAsync() }));
try
{
var idStr = await new StreamReader(context.Request.Body).ReadToEndAsync();
int processId = Convert.ToInt32(idStr);
Process[] processes = Process.GetProcesses().Where(p => p.Id == processId).ToArray(); // Telegram endpoints
app.MapGet($"{basePath}/telegram/status", async () =>
Results.Ok(new {
Enabled = await _telegramService.IsEnabledAsync(),
Connected = await _telegramService.TestConnectionAsync()
}));
if (processes.Length > 0) app.MapPost($"{basePath}/telegram/test", async () =>
{
foreach (var process in processes)
{
try
{
process.Kill();
await context.Response.WriteAsync($"Process with ID {processId} has been killed.");
}
catch (Exception ex)
{
await context.Response.WriteAsync($"Error killing process with ID {processId}: {ex.Message}");
}
}
}
else
{
await context.Response.WriteAsync($"No process found with ID {processId}.");
}
}
catch (Exception ex)
{
await context.Response.WriteAsync($"An error occurred: {ex.Message}");
}
});
/* curl -X POST http://localhost:5000/api/force-shutdown -d "5000" */
/* Invoke-WebRequest -Uri "http://localhost:5000/api/force-shutdown" -Method POST -Body "50000" -ContentType "text/plain" */
app.MapPost("/api/force-shutdown", async context =>
{ {
try try
{ {
var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); var testAlert = new Alert
var parameters = JsonConvert.DeserializeObject<dynamic>(requestBody);
string action = parameters?.Action?.ToString()?.ToLower(); // "shutdown" or "restart"
int delaySeconds = parameters?.DelaySeconds ?? 0;
// Validate action input
if (action != "shutdown" && action != "restart" && action != "cancel")
{ {
await context.Response.WriteAsync("Invalid action. Use 'shutdown', 'restart', or 'cancel'."); Timestamp = DateTime.Now,
return; Component = "Test",
Level = "Warning",
Message = "This is a test alert from Resource Monitor Service",
CurrentValue = 100,
ThresholdValue = 90,
IsResolved = false
};
await _telegramService.SendAlertAsync(testAlert);
return Results.Ok(new { Message = "Test alert sent successfully" });
}
catch (Exception ex)
{
return Results.Problem($"Failed to send test alert: {ex.Message}");
}
});
// 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 is stop, then cancel the shutdown
if (action == "cancel") if (action == "cancel")
{ {
var processStartInfoCancel = new ProcessStartInfo var cancelProcess = new ProcessStartInfo
{ {
FileName = "shutdown", FileName = "shutdown",
Arguments = "/a", Arguments = "/a",
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false
}; };
Process.Start(cancelProcess);
Process.Start(processStartInfoCancel);
await context.Response.WriteAsync("Shutdown cancelled."); _logger.LogWarning("System shutdown cancelled");
return; return Results.Ok(new { Message = "Shutdown cancelled." });
} }
// Validate delay input
if (delaySeconds < 0) if (delaySeconds < 0)
{ {
await context.Response.WriteAsync("Delay must be a non-negative integer."); return Results.BadRequest("Delay must be a non-negative integer.");
return;
} }
// Determine the shutdown command string shutdownCommand = action == "shutdown"
string shutdownCommand = action == "shutdown" ? $"/s /f /t {delaySeconds}" : $"/r /f /t {delaySeconds}"; ? $"/s /f /t {delaySeconds} /c \"{message}\""
: $"/r /f /t {delaySeconds} /c \"{message}\"";
var processStartInfo = new ProcessStartInfo var processStartInfo = new ProcessStartInfo
{ {
@@ -172,274 +286,233 @@ namespace ResourceMonitorService
Process.Start(processStartInfo); Process.Start(processStartInfo);
await context.Response.WriteAsync($"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds."); _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) catch (Exception ex)
{ {
await context.Response.WriteAsync($"An error occurred: {ex.Message}"); _logger.LogError(ex, "Error executing system command");
return Results.Problem(ex.Message);
} }
}); });
app.MapGet("/api/stop", async context => // VM-specific endpoints for Unraid
app.MapGet($"{basePath}/vm/info", async () =>
{ {
await context.Response.WriteAsync("Stopping the service..."); var systemInfo = await _systemInfoService.GetSystemInfoAsync();
_lifetime.StopApplication(); return Results.Ok(new
{
IsVirtualMachine = systemInfo.IsVirtualMachine,
HypervisorVendor = systemInfo.HypervisorVendor,
Uptime = systemInfo.Uptime,
BootTime = systemInfo.BootTime,
MachineName = systemInfo.MachineName,
Domain = systemInfo.Domain
});
}); });
app.MapGet("/", () => "Resource Monitor Service is running."); // Performance history endpoint (simple in-memory storage)
app.MapGet("/api/current-time", () => Results.Ok(GetCurrentTime())); app.MapGet($"{basePath}/performance/history", async (int minutes = 60) =>
app.MapGet("/api/computer-info", () => Results.Ok(GetComputerInfo())); {
app.MapGet("/api/cpu-usage", () => Results.Ok(GetCpuUsage())); // This would ideally be stored in a database or time-series database
app.MapGet("/api/ram-usage", () => Results.Ok(GetRamUsage())); // For now, return current snapshot with timestamp
app.MapGet("/api/gpu-usage", () => Results.Ok(GetGpuUsage())); var usage = await _resourceMonitorService.GetResourceUsageAsync();
app.MapGet("/api/running-game", () => Results.Ok(GetCurrentlyRunningGame())); return Results.Ok(new {
app.MapGet("/api/total-physical-memory", () => Results.Ok(GetTotalPhysicalMemory())); Current = usage,
app.MapGet("/api/total-available-memory", () => Results.Ok(new { TotalAvailableMemory = Environment.WorkingSet })); Message = "Historical data not implemented yet - showing current values"
});
});
app.MapGet("/health", () => Results.Ok("Service is healthy.")); // 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..." });
});
_ = app.RunAsync(stoppingToken); // Configuration endpoint
app.MapGet($"{basePath}/config", () => Results.Ok(new
{
MonitoringSettings = new
{
UpdateInterval = _monitoringSettings.UpdateIntervalMs,
EnableGpuMonitoring = _monitoringSettings.EnableGpuMonitoring,
EnableDiskMonitoring = _monitoringSettings.EnableDiskMonitoring,
EnableNetworkMonitoring = _monitoringSettings.EnableNetworkMonitoring,
EnableTemperatureMonitoring = _monitoringSettings.EnableTemperatureMonitoring,
EnableProcessMonitoring = _monitoringSettings.EnableProcessMonitoring,
EnableGameDetection = _monitoringSettings.EnableGameDetection,
EnableAlerts = _monitoringSettings.EnableAlerts
},
ApiSettings = new
{
BasePath = _apiSettings.BasePath,
RequireApiKey = _apiSettings.RequireApiKey,
AllowedOrigins = _apiSettings.AllowedOrigins
}
}));
await Task.Delay(Timeout.Infinite, stoppingToken); // Service metrics endpoint
app.MapGet($"{basePath}/metrics", async () =>
{
var usage = await _resourceMonitorService.GetResourceUsageAsync();
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
return Results.Ok(new
{
Service = "Resource Monitor Service",
Version = "2.0.0",
Status = "Running",
Timestamp = DateTime.Now,
Uptime = systemInfo.Uptime,
LastUpdate = usage.Timestamp,
Performance = new
{
CPU = usage.CPU.Usage,
Memory = usage.Memory.UsagePercentage,
GPU = usage.GPU.Usage,
ActiveProcesses = usage.TopProcesses.Count
}
});
});
// Root endpoint
app.MapGet("/", () => Results.Ok(new
{
Service = "Resource Monitor Service for Unraid VM",
Version = "2.0.0",
Status = "Running",
Timestamp = DateTime.Now,
ApiBasePath = _apiSettings.BasePath,
Endpoints = new[]
{
$"{_apiSettings.BasePath}/health",
$"{_apiSettings.BasePath}/system-info",
$"{_apiSettings.BasePath}/resource-usage",
$"{_apiSettings.BasePath}/cpu-usage",
$"{_apiSettings.BasePath}/memory-usage",
$"{_apiSettings.BasePath}/gpu-usage",
$"{_apiSettings.BasePath}/disk-usage",
$"{_apiSettings.BasePath}/network-usage",
$"{_apiSettings.BasePath}/top-processes",
$"{_apiSettings.BasePath}/current-game",
$"{_apiSettings.BasePath}/alerts/active",
$"{_apiSettings.BasePath}/vm/info",
$"{_apiSettings.BasePath}/config",
$"{_apiSettings.BasePath}/metrics"
}
}));
_logger.LogInformation("API endpoints configured. Base path: {BasePath}", _apiSettings.BasePath);
} }
private object GetComputerInfo() private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken)
{ {
return new _logger.LogInformation("Background monitoring started");
{ int errorCount = 0;
MachineName = Environment.MachineName, int successfulCycles = 0;
OSVersion = RuntimeInformation.OSDescription,
OSArchitecture = RuntimeInformation.OSArchitecture.ToString(),
ProcessorCount = Environment.ProcessorCount
};
}
private object GetCpuUsage() while (!cancellationToken.IsCancellationRequested)
{
#pragma warning disable CA1416 // Validate platform compatibility
var cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
#pragma warning restore CA1416 // Validate platform compatibility
#pragma warning disable CA1416 // Validate platform compatibility
cpuCounter.NextValue();
#pragma warning restore CA1416 // Validate platform compatibility
Thread.Sleep(1000); // Wait a second to get a valid reading
#pragma warning disable CA1416 // Validate platform compatibility
var usage = cpuCounter.NextValue();
#pragma warning restore CA1416 // Validate platform compatibility
if (usage > 80)
{ {
// Get the current processes and sort them by CPU usage in descending order try
var processes = Process.GetProcesses() {
.Select(p => // Get current resource usage
var resourceUsage = await _resourceMonitorService.GetResourceUsageAsync();
// Add current game info if game detection is enabled
if (_monitoringSettings.EnableGameDetection)
{ {
try try
{ {
return new resourceUsage.RunningGame = await _gameDetectionService.GetCurrentlyRunningGameAsync();
{
Process = p,
TotalProcessorTime = p.TotalProcessorTime
};
} }
catch catch (Exception ex)
{ {
return null; // Skip processes that throw exceptions // Only log game detection errors occasionally to avoid spam
} if (errorCount % 12 == 0) // Every minute if 5-second intervals
})
.Where(p => p != null)
.OrderByDescending(p => p.TotalProcessorTime)
.Select(p => p.Process)
.ToList();
// Create a new anonymous type containing the CPU usage, RAM usage, and the top 3 highest CPU-using processes
return new
{
Usage = usage,
Process1 = new
{
Name = processes.ElementAt(0).ProcessName,
TotalProcessorTime = processes.ElementAt(0).TotalProcessorTime,
WorkingSet64 = processes.ElementAt(0).WorkingSet64 / (1024 * 1024) // Convert to MB
},
Process2 = new
{
Name = processes.ElementAt(1).ProcessName,
TotalProcessorTime = processes.ElementAt(1).TotalProcessorTime,
WorkingSet64 = processes.ElementAt(1).WorkingSet64 / (1024 * 1024) // Convert to MB
},
Process3 = new
{
Name = processes.ElementAt(2).ProcessName,
TotalProcessorTime = processes.ElementAt(2).TotalProcessorTime,
WorkingSet64 = processes.ElementAt(2).WorkingSet64 / (1024 * 1024) // Convert to MB
}
};
}
return new
{
Usage = usage
};
}
private float GetRamUsage()
{
#pragma warning disable CA1416 // Validate platform compatibility
var ramCounter = new PerformanceCounter("Memory", "Available MBytes");
#pragma warning restore CA1416 // Validate platform compatibility
var totalMemory = GetTotalPhysicalMemory();
#pragma warning disable CA1416 // Validate platform compatibility
var availableMemory = ramCounter.NextValue() * 1024 * 1024;
#pragma warning restore CA1416 // Validate platform compatibility
return (float)(totalMemory - availableMemory) / totalMemory * 100;
}
private ulong GetTotalPhysicalMemory()
{
ulong totalMemory = 0;
#pragma warning disable CA1416 // Validate platform compatibility
var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
#pragma warning restore CA1416 // Validate platform compatibility
#pragma warning disable CA1416 // Validate platform compatibility
foreach (var obj in searcher.Get())
{
#pragma warning disable CA1416 // Validate platform compatibility
totalMemory = (ulong)obj["TotalPhysicalMemory"];
#pragma warning restore CA1416 // Validate platform compatibility
}
#pragma warning restore CA1416 // Validate platform compatibility
return totalMemory;
}
private object GetGpuUsage()
{
/* if (!IsNvidiaGpuPresent())
{
return new
{
Usage = 0,
Temperature = 0,
FanSpeed = 0,
IsAvailable = false,
Message = "No NVIDIA GPU detected"
};
} */
try
{
NvmlWrapper.NvmlInit();
IntPtr device;
NvmlWrapper.NvmlDeviceGetHandleByIndex(0, out device);
NvmlWrapper.NvmlUtilization utilization;
NvmlWrapper.NvmlDeviceGetUtilizationRates(device, out utilization);
uint temperature;
NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature);
uint fanSpeed;
NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed);
NvmlWrapper.NvmlShutdown();
return new
{
Usage = utilization.Gpu,
Temperature = temperature,
FanSpeed = fanSpeed,
IsAvailable = false,
Error = ""
};
}
catch (Exception ex)
{
return new
{
Usage = 0,
Temperature = 0,
FanSpeed = 0,
IsAvailable = false,
Error = ex.Message
};
}
}
/* private bool IsNvidiaGpuPresent()
{
try
{
// Method 1: Try to initialize NVML
NvmlWrapper.NvmlInit();
uint deviceCount = 0;
NvmlWrapper.NvmlDeviceGetCount(ref deviceCount);
NvmlWrapper.NvmlShutdown();
return deviceCount > 0;
}
catch
{
// Method 2: Fallback to checking using WMI
try
{
using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController WHERE Name LIKE '%NVIDIA%'"))
{
var collection = searcher.Get();
return collection.Count > 0;
}
}
catch
{
return false;
}
}
} */
private object GetCurrentlyRunningGame()
{
var processes = Process.GetProcesses();
foreach (var process in processes)
{
try
{
#pragma warning disable CS8602 // Dereference of a possibly null reference.
var filePath = process.MainModule.FileName;
#pragma warning restore CS8602 // Dereference of a possibly null reference.
if (filePath.Contains(@"\steamapps\common\"))
{
// Extract the game directory name
var parts = filePath.Split(new[] { @"\steamapps\common\" }, StringSplitOptions.None);
if (parts.Length > 1)
{
var gamePath = parts[1];
var gameName = gamePath.Split(Path.DirectorySeparatorChar)[0];
return new
{ {
GameName = gameName, _logger.LogDebug("Game detection error (suppressed): {Message}", ex.Message);
ExecutableName = Path.GetFileName(filePath), }
FullPath = filePath,
ProcessId = process.Id,
MemoryUsage = process.WorkingSet64 / (1024 * 1024) + " MB", // Memory usage in MB
CpuTime = process.TotalProcessorTime.ToString(),
StartTime = process.StartTime.ToString("G"), // General date/time pattern
UserName = Environment.UserName // The user running the process
};
} }
} }
// Check for alerts
if (_monitoringSettings.EnableAlerts)
{
await _alertService.CheckAndGenerateAlertsAsync(resourceUsage);
}
successfulCycles++;
// Log performance metrics occasionally
if (successfulCycles % 6 == 0) // Every 30 seconds with 5-second intervals
{
_logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%",
resourceUsage.CPU.Usage,
resourceUsage.Memory.UsagePercentage,
resourceUsage.GPU.Usage);
}
// Log successful monitoring occasionally for health verification
if (successfulCycles % 120 == 0) // Every 10 minutes
{
_logger.LogInformation("Background monitoring healthy - completed {SuccessfulCycles} cycles", successfulCycles);
}
errorCount = 0; // Reset error count on success
} }
catch (Exception) catch (Exception ex)
{ {
// Handle access exceptions or continue if not important errorCount++;
// Only log errors occasionally to avoid spam, but always log the first few
if (errorCount <= 3 || errorCount % 12 == 0)
{
_logger.LogError(ex, "Error in background monitoring loop (occurrence #{ErrorCount})", errorCount);
}
// If too many consecutive errors, increase delay
if (errorCount > 10)
{
await Task.Delay(_monitoringSettings.UpdateIntervalMs * 2, cancellationToken);
continue;
}
} }
await Task.Delay(_monitoringSettings.UpdateIntervalMs, cancellationToken);
} }
return "No Steam game is currently running.";
}
private string GetCurrentTime()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
_logger.LogInformation("Background monitoring stopped after {SuccessfulCycles} successful cycles", successfulCycles);
}
} }
} }
+96 -3
View File
@@ -2,18 +2,111 @@
"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,
"Kestrel": { "Kestrel": {
"Endpoints": { "Endpoints": {
"Http": { "Http": {
"Url": "http://*:5000" "Url": "http://*:2414"
} }
} }
}, },
"ApiSettings": { "ApiSettings": {
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" "ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a",
"RequireApiKey": false,
"AllowedOrigins": [
"http://localhost:4200",
"http://192.168.50.52:4200",
"http://vmwin11:4200",
"http://unraid:4200"
],
"EnableSwagger": false,
"BasePath": "/api"
},
"MonitoringSettings": {
"UpdateIntervalMs": 5000,
"DataRetentionDays": 7,
"EnableGpuMonitoring": true,
"EnableDiskMonitoring": true,
"EnableNetworkMonitoring": true,
"EnableTemperatureMonitoring": true,
"EnableProcessMonitoring": true,
"EnableGameDetection": true,
"EnableAlerts": true,
"MaxProcessesToTrack": 10,
"MaxHistoryPoints": 1000,
"GamePlatformPaths": [
"\\steamapps\\common\\",
"\\Epic Games\\",
"\\GOG Galaxy\\Games\\",
"\\Origin Games\\",
"\\Ubisoft Game Launcher\\games\\"
],
"GameRootFolders": [
"C:\\Games",
"D:\\Games",
"E:\\Games"
],
"AlertThresholds": [
{
"Component": "CPU",
"WarningThreshold": 80,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "Memory",
"WarningThreshold": 85,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "GPU",
"WarningThreshold": 85,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "CPUTemp",
"WarningThreshold": 75,
"CriticalThreshold": 85,
"DurationSeconds": 60,
"IsEnabled": true
},
{
"Component": "GPUTemp",
"WarningThreshold": 80,
"CriticalThreshold": 90,
"DurationSeconds": 60,
"IsEnabled": true
}
],
"Telegram": {
"IsEnabled": true,
"BotToken": "7705627522:AAHDTVMF1uPJW7qm-Di0g_BmefAVWdOrS2U",
"ChatIds": [398126624],
"SendWarningAlerts": true,
"SendCriticalAlerts": true,
"SendResolutionNotifications": true,
"MessageTemplate": "🚨 *{Level} Alert*\n\n📊 *{Component}*\n💬 {Message}\n⏰ {Timestamp}",
"ResolutionTemplate": "✅ *Alert Resolved*\n\n📊 *{Component}*\n💬 {Message}\n⏰ Resolved at {ResolvedAt}"
}
},
"LoggingSettings": {
"LogLevel": "Information",
"LogPath": "logs",
"MaxLogFiles": 30,
"MaxLogFileSizeMB": 10,
"EnableFileLogging": true,
"EnableConsoleLogging": true,
"EnablePerformanceLogging": false
} }
} }
+74
View File
@@ -0,0 +1,74 @@
{
"MonitoringSettings": {
"UpdateIntervalMs": 5000,
"DataRetentionDays": 7,
"EnableGpuMonitoring": true,
"EnableDiskMonitoring": true,
"EnableNetworkMonitoring": true,
"EnableTemperatureMonitoring": true,
"EnableProcessMonitoring": true,
"EnableGameDetection": true,
"EnableAlerts": true,
"MaxProcessesToTrack": 10,
"MaxHistoryPoints": 1000,
"GamePlatformPaths": [
"\\steamapps\\common\\",
"\\Epic Games\\",
"\\GOG Galaxy\\Games\\",
"\\Origin Games\\",
"\\Ubisoft Game Launcher\\games\\"
],
"GameRootFolders": [
"C:\\Games",
"D:\\Games",
"E:\\Games"
],
"AlertThresholds": [
{
"Component": "CPU",
"WarningThreshold": 80,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "Memory",
"WarningThreshold": 85,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "GPU",
"WarningThreshold": 85,
"CriticalThreshold": 95,
"DurationSeconds": 30,
"IsEnabled": true
},
{
"Component": "CPUTemp",
"WarningThreshold": 75,
"CriticalThreshold": 85,
"DurationSeconds": 60,
"IsEnabled": true
},
{
"Component": "GPUTemp",
"WarningThreshold": 80,
"CriticalThreshold": 90,
"DurationSeconds": 60,
"IsEnabled": true
}
],
"Telegram": {
"IsEnabled": true,
"BotToken": "123456789:ABCdefGHIjklMNOpqrSTUvwxyz",
"ChatIds": [123456789],
"SendWarningAlerts": true,
"SendCriticalAlerts": true,
"SendResolutionNotifications": true,
"MessageTemplate": "🚨 *{Level} Alert*\n\n📊 *{Component}*\n💬 {Message}\n⏰ {Timestamp}",
"ResolutionTemplate": "✅ *Alert Resolved*\n\n📊 *{Component}*\n💬 {Message}\n⏰ Resolved at {ResolvedAt}"
}
}
}
+193
View File
@@ -0,0 +1,193 @@
# Resource Monitor Service - Installation Script for Windows Service
# Run this in PowerShell as Administrator
param(
[switch]$Uninstall
)
$SERVICE_NAME = "ResourceMonitorService"
$SERVICE_DISPLAY_NAME = "Resource Monitor Service v2.0"
$SERVICE_DESCRIPTION = "Monitors VM resources for Unraid integration"
$INSTALL_PATH = "C:\Services\ResourceMonitor"
Write-Host "=== Resource Monitor Service - Windows Service Installer ===" -ForegroundColor Cyan
Write-Host
# Check if running as administrator
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "ERROR: This script must be run as Administrator" -ForegroundColor Red
Write-Host "Please run PowerShell as Administrator and try again" -ForegroundColor Yellow
exit 1
}
if ($Uninstall) {
Write-Host "Uninstalling Resource Monitor Service..." -ForegroundColor Yellow
# Stop the service
Write-Host "Stopping service..."
try {
Stop-Service -Name $SERVICE_NAME -Force -ErrorAction SilentlyContinue
Write-Host "Service stopped successfully" -ForegroundColor Green
} catch {
Write-Host "Service was not running" -ForegroundColor Yellow
}
# Remove the service
Write-Host "Removing service..."
try {
sc.exe delete $SERVICE_NAME
Write-Host "Service removed successfully" -ForegroundColor Green
} catch {
Write-Host "Failed to remove service: $($_.Exception.Message)" -ForegroundColor Red
}
# Remove firewall rule
Write-Host "Removing firewall rule..."
try {
Remove-NetFirewallRule -DisplayName "Resource Monitor Service" -ErrorAction SilentlyContinue
Write-Host "Firewall rule removed" -ForegroundColor Green
} catch {
Write-Host "Firewall rule not found or already removed" -ForegroundColor Yellow
}
# Optionally remove installation directory
$removeFiles = Read-Host "Remove installation files from $INSTALL_PATH? (y/N)"
if ($removeFiles -eq "y" -or $removeFiles -eq "Y") {
try {
Remove-Item -Path $INSTALL_PATH -Recurse -Force -ErrorAction Stop
Write-Host "Installation files removed" -ForegroundColor Green
} catch {
Write-Host "Failed to remove installation files: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host "Uninstallation complete!" -ForegroundColor Green
exit 0
}
Write-Host "Installing Resource Monitor Service as Windows Service..." -ForegroundColor Green
# Create installation directory
Write-Host "Creating installation directory..."
try {
New-Item -ItemType Directory -Path $INSTALL_PATH -Force | Out-Null
Write-Host "Installation directory created: $INSTALL_PATH" -ForegroundColor Green
} catch {
Write-Host "ERROR: Failed to create installation directory: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Build the service in release mode
Write-Host "Building service..."
try {
$buildResult = dotnet publish --configuration Release --output $INSTALL_PATH
if ($LASTEXITCODE -ne 0) {
throw "Build failed with exit code $LASTEXITCODE"
}
Write-Host "Service built successfully" -ForegroundColor Green
} catch {
Write-Host "ERROR: Build failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Stop existing service if running
Write-Host "Stopping existing service (if running)..."
try {
Stop-Service -Name $SERVICE_NAME -Force -ErrorAction SilentlyContinue
Write-Host "Existing service stopped" -ForegroundColor Green
} catch {
Write-Host "No existing service found" -ForegroundColor Yellow
}
# Remove existing service if it exists
Write-Host "Removing existing service (if exists)..."
try {
sc.exe delete $SERVICE_NAME 2>$null
} catch {
# Ignore errors if service doesn't exist
}
# Install the service
Write-Host "Installing Windows Service..."
try {
$serviceBinPath = "`"$INSTALL_PATH\ResourceMonitorService.exe`" --windows-service"
$createResult = sc.exe create $SERVICE_NAME binPath= $serviceBinPath DisplayName= $SERVICE_DISPLAY_NAME start= auto
if ($LASTEXITCODE -ne 0) {
throw "Failed to create service with exit code $LASTEXITCODE"
}
# Set service description
sc.exe description $SERVICE_NAME $SERVICE_DESCRIPTION
Write-Host "Windows Service created successfully" -ForegroundColor Green
} catch {
Write-Host "ERROR: Failed to create Windows Service: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Configure service recovery options
Write-Host "Configuring service recovery options..."
try {
sc.exe failure $SERVICE_NAME reset= 300 actions= restart/5000/restart/5000/restart/10000
Write-Host "Service recovery options configured" -ForegroundColor Green
} catch {
Write-Host "WARNING: Failed to configure service recovery options" -ForegroundColor Yellow
}
# Configure firewall rule
Write-Host "Configuring Windows Firewall..."
try {
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow -Profile Any -ErrorAction Stop
Write-Host "Firewall rule created" -ForegroundColor Green
} catch {
Write-Host "WARNING: Failed to create firewall rule. You may need to configure manually." -ForegroundColor Yellow
Write-Host "Manual command: New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow" -ForegroundColor Gray
}
# Start the service
Write-Host "Starting service..."
try {
Start-Service -Name $SERVICE_NAME
Write-Host "Service started successfully" -ForegroundColor Green
} catch {
Write-Host "ERROR: Failed to start service: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Check Windows Event Log for details" -ForegroundColor Yellow
exit 1
}
# Wait a moment and check service status
Start-Sleep -Seconds 3
$serviceStatus = Get-Service -Name $SERVICE_NAME
Write-Host "Service Status: $($serviceStatus.Status)" -ForegroundColor $(if ($serviceStatus.Status -eq "Running") { "Green" } else { "Red" })
Write-Host
Write-Host "=== Installation Complete ===" -ForegroundColor Cyan
Write-Host "Service Name: $SERVICE_NAME" -ForegroundColor White
Write-Host "Installation Path: $INSTALL_PATH" -ForegroundColor White
Write-Host "Service URL: http://localhost:2414" -ForegroundColor White
Write-Host "API Health Check: http://localhost:2414/api/health" -ForegroundColor White
Write-Host
Write-Host "The service is now running and will start automatically with Windows." -ForegroundColor Green
Write-Host "You can manage it through Services.msc or using PowerShell commands:" -ForegroundColor White
Write-Host " - Stop: Stop-Service -Name $SERVICE_NAME" -ForegroundColor Gray
Write-Host " - Start: Start-Service -Name $SERVICE_NAME" -ForegroundColor Gray
Write-Host " - Status: Get-Service -Name $SERVICE_NAME" -ForegroundColor Gray
Write-Host " - Restart: Restart-Service -Name $SERVICE_NAME" -ForegroundColor Gray
Write-Host
Write-Host "To uninstall: .\install-service.ps1 -Uninstall" -ForegroundColor Yellow
# Test the API endpoint
Write-Host
Write-Host "Testing API endpoint..." -ForegroundColor Yellow
Start-Sleep -Seconds 5
try {
$response = Invoke-RestMethod -Uri "http://localhost:2414/" -TimeoutSec 10
Write-Host "API Test Result: SUCCESS" -ForegroundColor Green
Write-Host "Service Version: $($response.Service) v$($response.Version)" -ForegroundColor White
Write-Host "Status: $($response.Status)" -ForegroundColor White
} catch {
Write-Host "API Test Result: FAILED" -ForegroundColor Red
Write-Host "The service may still be starting up. Wait a few minutes and try accessing:" -ForegroundColor Yellow
Write-Host "http://localhost:2414/api/health" -ForegroundColor White
}
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# Resource Monitor Service - Installation Script for Windows Service
SERVICE_NAME="ResourceMonitorService"
SERVICE_DISPLAY_NAME="Resource Monitor Service v2.0"
SERVICE_DESCRIPTION="Monitors VM resources for Unraid integration"
INSTALL_PATH="C:\Services\ResourceMonitor"
echo "=== Resource Monitor Service - Windows Service Installer ==="
echo
# Check if running as administrator
if [[ ! $(id -u) -eq 0 ]]; then
echo "ERROR: This script must be run as Administrator"
echo "Please run PowerShell as Administrator and try again"
exit 1
fi
echo "Installing Resource Monitor Service as Windows Service..."
# Create installation directory
echo "Creating installation directory..."
mkdir -p "$INSTALL_PATH"
# Build the service in release mode
echo "Building service..."
dotnet publish --configuration Release --output "$INSTALL_PATH"
if [[ $? -ne 0 ]]; then
echo "ERROR: Build failed"
exit 1
fi
# Stop existing service if running
echo "Stopping existing service (if running)..."
sc stop "$SERVICE_NAME" 2>/dev/null
# Remove existing service if it exists
echo "Removing existing service (if exists)..."
sc delete "$SERVICE_NAME" 2>/dev/null
# Install the service
echo "Installing Windows Service..."
sc create "$SERVICE_NAME" \
binPath="\"$INSTALL_PATH\\ResourceMonitorService.exe\" --windows-service" \
DisplayName="$SERVICE_DISPLAY_NAME" \
Description="$SERVICE_DESCRIPTION" \
start=auto
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create Windows Service"
exit 1
fi
# Configure service recovery options
echo "Configuring service recovery options..."
sc failure "$SERVICE_NAME" reset=300 actions=restart/5000/restart/5000/restart/10000
# Configure firewall rule
echo "Configuring Windows Firewall..."
powershell -Command "New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow -Profile Any" 2>/dev/null
# Start the service
echo "Starting service..."
sc start "$SERVICE_NAME"
if [[ $? -eq 0 ]]; then
echo
echo "=== Installation Complete ==="
echo "Service Name: $SERVICE_NAME"
echo "Installation Path: $INSTALL_PATH"
echo "Service URL: http://localhost:2414"
echo "API Health Check: http://localhost:2414/api/health"
echo
echo "The service is now running and will start automatically with Windows."
echo "You can manage it through Services.msc or using sc commands:"
echo " - Stop: sc stop $SERVICE_NAME"
echo " - Start: sc start $SERVICE_NAME"
echo " - Status: sc query $SERVICE_NAME"
echo
echo "To uninstall: sc stop $SERVICE_NAME && sc delete $SERVICE_NAME"
else
echo "ERROR: Failed to start service"
echo "Check Windows Event Log for details"
exit 1
fi
-8
View File
@@ -1,8 +0,0 @@
# PowerShell script to create a new inbound rule in Windows Firewall
$port = 5000
$ruleName = "ResourceMonitorServicePublish"
if (Get-NetFirewallRule -DisplayName $ruleName) {
Write-Host "Rule already exists, not creating a new one"
} else {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol TCP -LocalPort $port -Action Allow -Profile Any
}
-4
View File
@@ -1,4 +0,0 @@
sc create ResourceMonitorService binPath="%~dp0ResourceMonitorService.exe --windows-service" start= auto
sc description ResourceMonitorService "A service that monitors system resource usage and exposes it via a web API."
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RunAsWindowsService": true,
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
}
}
},
"ApiSettings": {
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
}
}
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RunAsWindowsService": true,
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
}
}
},
"ApiSettings": {
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
}
}
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
@@ -1,19 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RunAsWindowsService": true,
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
}
}
},
"ApiSettings": {
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
}
}
+41
View File
@@ -0,0 +1,41 @@
@echo off
REM Resource Monitor Service - Quick Start Script
echo Starting Resource Monitor Service v2.0...
echo.
echo This service will monitor your VM's resources and provide a REST API
echo for remote monitoring from your Unraid server.
echo.
echo Service will be available at: http://localhost:5000
echo API Documentation: http://localhost:5000/api/health
echo.
echo Press Ctrl+C to stop the service
echo.
REM Change to the script's directory
cd /d "%~dp0"
REM Check if .NET 9 is installed
dotnet --version > nul 2>&1
if errorlevel 1 (
echo ERROR: .NET 9.0 Runtime is required but not found.
echo Please install .NET 9.0 Runtime from: https://dotnet.microsoft.com/download
pause
exit /b 1
)
REM Build and run the service
echo Building service...
dotnet build --configuration Release
if errorlevel 1 (
echo ERROR: Build failed. Please check the error messages above.
pause
exit /b 1
)
echo.
echo Starting service on http://localhost:5000
echo.
dotnet run --configuration Release
pause