21 Commits

Author SHA1 Message Date
king 3d1c55468b Enhance monitoring features and UI:
- Add detailed CPU core monitoring option for better performance control
- Update monitoring settings in app configuration
- Improve parallel task execution for resource usage monitoring
- Modify Telegram notification service to skip alerts from svchost processes
- Add "Memory %" column to process table in HTML and update related JavaScript
- Create performance test scripts for API response time evaluation
2025-08-08 16:19:45 +08:00
king bb7c4c3d0e Refactor code structure for improved readability and maintainability 2025-08-08 12:26:54 +08:00
king eceec1b72d Add cancel shutdown feature and enhance system control access methods in dashboard 2025-08-08 12:00:42 +08:00
king 1129f9a2b1 Add system control feature for remote shutdown/restart with timer support and UI integration 2025-08-08 11:52:12 +08:00
king 5ece1fbe27 Remove network monitoring features and related code; update GPU monitoring in dashboard for improved performance and clarity. 2025-08-08 11:45:48 +08:00
Phoenix 35828f189c Implement structural adjustments and optimize performance across multiple modules 2025-08-07 23:25:01 +08:00
Phoenix e842b7d73e Increase update interval to 120 seconds and adjust auto-refresh fallback to 300 seconds for improved performance. 2025-08-07 23:14:59 +08:00
Phoenix 96b6e3dcd9 Update README and installation scripts for Resource Monitor Service v2.1, enhancing web dashboard features and adjusting firewall configurations. 2025-08-07 23:10:40 +08:00
Phoenix 3d47fc1439 Add ResourceHub for real-time updates and implement web dashboard with REST API
- Created ResourceHub.cs for SignalR group management.
- Developed a modern web dashboard using Tailwind CSS for responsive design.
- Implemented real-time updates with SignalR for CPU, Memory, GPU, and Network usage.
- Added REST API endpoints for resource information and process management.
- Integrated process management features to view and terminate high-usage processes.
- Enhanced UI with loading spinners, notifications, and responsive tables.
- Included performance charts for historical CPU and Memory usage.
- Configured Swagger UI for API documentation.
- Established security features including process kill restrictions and API key authentication.
2025-08-07 23:02:03 +08:00
king aa30c9f034 Merge pull request 'feature/telegram-alert' (#4) from feature/telegram-alert into master
Reviewed-on: #4
2025-08-07 17:54:32 +08:00
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
king ea2fe47263 Merge pull request 'feature/laptop' (#2) from feature/laptop into master
Reviewed-on: #2
2025-04-30 17:08:08 +08:00
king 294438145a Refactor NVML wrapper and update CORS policy; add appsettings configuration files 2025-04-30 17:01:33 +08:00
king 413360ece2 Fix service name in firewall configuration and installation scripts 2025-04-28 15:37:53 +08:00
king a0b9f05ae3 Implement API key validation middleware and add force shutdown endpoint 2025-04-28 15:16:44 +08:00
sothman01 caa7436d51 Add GPU detection and usage retrieval with error handling 2025-04-28 13:42:16 +08:00
48 changed files with 6671 additions and 458 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; } = 15000; // 15 seconds
public int DataRetentionDays { get; set; } = 7;
public bool EnableGpuMonitoring { get; set; } = true;
public bool EnableDiskMonitoring { get; set; } = true;
public bool EnableTemperatureMonitoring { get; set; } = true;
public bool EnableProcessMonitoring { get; set; } = true;
public bool EnableDetailedCpuCoreMonitoring { get; set; } = false; // Disable by default for better performance
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}";
}
}
+280
View File
@@ -0,0 +1,280 @@
using Microsoft.AspNetCore.Mvc;
using ResourceMonitorService.Models;
using ResourceMonitorService.Services;
using System.Diagnostics;
namespace ResourceMonitorService.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private readonly IResourceMonitorService _resourceMonitorService;
private readonly ISystemInfoService _systemInfoService;
private readonly ILogger<ResourceController> _logger;
public ResourceController(
IResourceMonitorService resourceMonitorService,
ISystemInfoService systemInfoService,
ILogger<ResourceController> logger)
{
_resourceMonitorService = resourceMonitorService;
_systemInfoService = systemInfoService;
_logger = logger;
}
[HttpGet("usage")]
public async Task<ActionResult<ResourceUsage>> GetResourceUsage()
{
try
{
var usage = await _resourceMonitorService.GetResourceUsageAsync();
return Ok(usage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting resource usage");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("system-info")]
public async Task<ActionResult<SystemInfo>> GetSystemInfo()
{
try
{
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
return Ok(systemInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting system info");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("cpu")]
public async Task<ActionResult<CpuUsage>> GetCpuUsage()
{
try
{
var cpuUsage = await _resourceMonitorService.GetCpuUsageAsync();
return Ok(cpuUsage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting CPU usage");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("memory")]
public async Task<ActionResult<MemoryUsage>> GetMemoryUsage()
{
try
{
var memoryUsage = await _resourceMonitorService.GetMemoryUsageAsync();
return Ok(memoryUsage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting memory usage");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("gpu")]
public async Task<ActionResult<GpuUsage>> GetGpuUsage()
{
try
{
var gpuUsage = await _resourceMonitorService.GetGpuUsageAsync();
return Ok(gpuUsage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting GPU usage");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("disks")]
public async Task<ActionResult<List<DiskUsage>>> GetDiskUsage()
{
try
{
var diskUsage = await _resourceMonitorService.GetDiskUsageAsync();
return Ok(diskUsage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting disk usage");
return StatusCode(500, "Internal server error");
}
}
[HttpGet("processes")]
public async Task<ActionResult<List<ProcessInfo>>> GetTopProcesses([FromQuery] int count = 10)
{
try
{
var processes = await _resourceMonitorService.GetTopProcessesAsync(count);
return Ok(processes);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting top processes");
return StatusCode(500, "Internal server error");
}
}
[HttpPost("kill-process/{processId}")]
public ActionResult KillProcess(int processId)
{
try
{
var process = Process.GetProcessById(processId);
if (process == null)
{
return NotFound($"Process with ID {processId} not found");
}
process.Kill();
_logger.LogInformation($"Process {process.ProcessName} (ID: {processId}) was terminated");
return Ok(new { message = $"Process {process.ProcessName} (ID: {processId}) was terminated successfully" });
}
catch (ArgumentException)
{
return NotFound($"Process with ID {processId} not found");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error killing process {processId}");
return StatusCode(500, "Internal server error");
}
}
[HttpPost("system-control")]
public ActionResult SystemControl([FromBody] SystemControlRequest request)
{
try
{
if (request == null)
{
return BadRequest("Invalid request");
}
// Validate action
if (request.Action != "shutdown" && request.Action != "restart")
{
return BadRequest("Invalid action. Use 'shutdown' or 'restart'");
}
// Validate timer
if (request.Timer < 0 || request.Timer > 86400)
{
return BadRequest("Timer must be between 0 and 86400 seconds (24 hours)");
}
// Build the shutdown command
var arguments = request.Action == "shutdown" ? "/s" : "/r";
if (request.Force)
{
arguments += " /f";
}
if (request.Timer > 0)
{
arguments += $" /t {request.Timer}";
}
// Execute the shutdown command
var processInfo = new ProcessStartInfo
{
FileName = "shutdown",
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processInfo);
string message;
if (request.Timer > 0)
{
var minutes = request.Timer / 60;
var seconds = request.Timer % 60;
var timeString = minutes > 0
? $"{minutes} minute{(minutes != 1 ? "s" : "")}" + (seconds > 0 ? $" and {seconds} second{(seconds != 1 ? "s" : "")}" : "")
: $"{seconds} second{(seconds != 1 ? "s" : "")}";
message = $"System {request.Action} scheduled in {timeString}";
}
else
{
message = $"System {request.Action} initiated immediately";
}
_logger.LogWarning($"System {request.Action} command executed by user. Timer: {request.Timer}s, Force: {request.Force}");
return Ok(new { message });
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error executing system {request?.Action} command");
return StatusCode(500, "Internal server error");
}
}
[HttpPost("cancel-shutdown")]
public ActionResult CancelShutdown()
{
try
{
// Execute the shutdown abort command
var processInfo = new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/a",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
var process = Process.Start(processInfo);
process?.WaitForExit();
var exitCode = process?.ExitCode ?? -1;
string message;
if (exitCode == 0)
{
message = "Shutdown/restart canceled successfully";
_logger.LogInformation("Shutdown/restart canceled by user");
}
else
{
message = "No shutdown/restart was scheduled to cancel, or cancellation failed";
_logger.LogInformation("Attempted to cancel shutdown but none was scheduled");
}
return Ok(new { message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error canceling shutdown command");
return StatusCode(500, "Internal server error");
}
}
}
public class SystemControlRequest
{
public string Action { get; set; } = string.Empty;
public int Timer { get; set; } = 0;
public bool Force { get; set; } = true;
}
}
+226
View File
@@ -0,0 +1,226 @@
# ResourceMonitorService - Packaging and VM Deployment Guide
This guide explains how to package the ResourceMonitorService for release and deploy it to a VM.
## 📦 Packaging for Release
### Quick Packaging
```powershell
# Simple packaging - creates a ZIP file with all necessary components
.\create-package.ps1
# Specify version
.\create-package.ps1 -Version "2.1.1"
```
### What Gets Packaged
The packaging script includes:
- ✅ Compiled .NET binaries (Release build)
- ✅ Installation scripts (`install-service.ps1`, `start-service.bat`)
- ✅ Configuration files (`appsettings.json`)
- ✅ Documentation files (`README.md`, etc.)
- ✅ Deployment instructions (`DEPLOYMENT.txt`)
### Output
- **Location**: `.\release-packages\`
- **Format**: `ResourceMonitorService-v{VERSION}-{TIMESTAMP}.zip`
- **Size**: ~2.3 MB
- **Example**: `ResourceMonitorService-v2.1.0-20250807-2322.zip`
## 🚀 VM Deployment Options
### Option 1: Manual Deployment (Recommended)
1. **Transfer the ZIP file to your VM**:
- Copy via RDP shared folders
- Use network file share
- Download from cloud storage
- USB transfer
2. **On the VM**:
```powershell
# Extract the ZIP file to a temporary directory
Expand-Archive -Path "ResourceMonitorService-v2.1.0-*.zip" -DestinationPath "C:\Temp\ResourceMonitor"
# Navigate to extracted directory
cd "C:\Temp\ResourceMonitor"
# Run installation as Administrator
.\install-service.ps1
```
3. **Access the service**:
- Web Dashboard: `http://VM-IP:5000`
- API Health: `http://VM-IP:5000/api/health`
### Option 2: Automated Deployment (Advanced)
If your VM has PowerShell Remoting enabled:
```powershell
# Deploy using WinRM (requires setup)
.\deploy-to-vm.ps1 -VMAddress "192.168.1.100" -UseWinRM
# Copy only (no auto-install)
.\deploy-to-vm.ps1 -VMAddress "192.168.1.100" -UseWinRM -CopyOnly
```
### Option 3: Complete Build & Deploy
```powershell
# Build, package, and deploy in one command
.\build-and-deploy.ps1 -VMAddress "192.168.1.100" -DeployToVM -UseWinRM
```
## 🔧 VM Prerequisites
### Required Software
- **Windows 10/11 or Windows Server 2019+**
- **.NET 9.0 Runtime** ([Download](https://dotnet.microsoft.com/download/dotnet/9.0))
- **PowerShell 5.1+** (Built into Windows)
### Administrator Privileges
The installation requires Administrator privileges to:
- Create Windows Service
- Configure firewall rules
- Create directories in Program Files
- Set service permissions
### Network Requirements
- **Port 5000**: Web Dashboard access
- **Port 5001**: HTTPS access (optional)
- Firewall rule is automatically created during installation
## 📋 Installation Process
The `install-service.ps1` script automatically:
1. ✅ **Creates installation directory** (`C:\Services\ResourceMonitor`)
2. ✅ **Copies all service files**
3. ✅ **Registers Windows Service** (`ResourceMonitorService`)
4. ✅ **Configures auto-start** (starts with Windows)
5. ✅ **Creates firewall rule** (port 5000)
6. ✅ **Starts the service**
7. ✅ **Tests web dashboard** availability
## 🎯 Post-Installation
### Service Management
```powershell
# Check service status
Get-Service ResourceMonitorService
# Start/Stop/Restart service
Start-Service ResourceMonitorService
Stop-Service ResourceMonitorService
Restart-Service ResourceMonitorService
# Uninstall service
.\install-service.ps1 -Uninstall
```
### Access Points
- **Web Dashboard**: `http://VM-IP:5000`
- **API Documentation**: `http://VM-IP:5000/swagger` (if enabled)
- **Health Check**: `http://VM-IP:5000/api/health`
- **Logs**: `C:\Services\ResourceMonitor\logs\`
### Configuration
Edit `C:\Services\ResourceMonitor\appsettings.json` to customize:
- Monitoring intervals
- Alert thresholds
- Telegram notifications
- API settings
- Logging levels
## 🔍 Troubleshooting
### Common Issues
**Service won't start**:
```powershell
# Check Windows Event Log
Get-EventLog -LogName Application -Source "ResourceMonitorService" -Newest 10
# Check service logs
Get-Content "C:\Services\ResourceMonitor\logs\*.txt" -Tail 50
```
**Port 5000 not accessible**:
```powershell
# Manually create firewall rule
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow
# Check if port is listening
netstat -an | findstr :5000
```
**.NET Runtime not found**:
```powershell
# Check .NET installation
dotnet --version
dotnet --list-runtimes
# Download from: https://dotnet.microsoft.com/download/dotnet/9.0
```
### Log Locations
- **Service Logs**: `C:\Services\ResourceMonitor\logs\`
- **Windows Event Log**: Application > ResourceMonitorService
- **Installation Logs**: Console output during installation
## 📝 File Structure
After installation, the service directory contains:
```
C:\Services\ResourceMonitor\
├── ResourceMonitorService.exe # Main service executable
├── ResourceMonitorService.dll # Application library
├── appsettings.json # Configuration file
├── appsettings.Development.json # Development settings
├── install-service.ps1 # Installation script
├── start-service.bat # Manual start script
├── DEPLOYMENT.txt # Deployment instructions
├── logs\ # Log files directory
├── wwwroot\ # Web dashboard files
│ ├── index.html
│ ├── css\
│ └── js\
└── [various .NET runtime files]
```
## 🚀 Quick Reference
### Essential Commands
```powershell
# Package for deployment
.\create-package.ps1
# Install on VM (as Administrator)
.\install-service.ps1
# Check service status
Get-Service ResourceMonitorService
# Access dashboard
Start-Process "http://localhost:5000"
# Uninstall
.\install-service.ps1 -Uninstall
```
### Network Access
Replace `localhost` with your VM's IP address to access remotely:
- `http://192.168.1.100:5000` (Web Dashboard)
- `http://192.168.1.100:5000/api/health` (Health Check)
---
## 🔗 Additional Resources
- **Main README**: `README.md`
- **Web UI Guide**: `README_WebUI.md`
- **Telegram Setup**: `TELEGRAM_SETUP.md`
- **Project Repository**: [GitHub/ResourceMonitorService]
For support or issues, check the troubleshooting section above or review the service logs.
+17
View File
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.SignalR;
namespace ResourceMonitorService.Hubs
{
public class ResourceHub : Hub
{
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
}
}
+149
View File
@@ -0,0 +1,149 @@
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 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 ProcessInfo
{
public int ProcessId { 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; }
}
}
+26
View File
@@ -9,6 +9,15 @@ public static class NvmlWrapper
[DllImport("nvml.dll", EntryPoint = "nvmlShutdown")] [DllImport("nvml.dll", EntryPoint = "nvmlShutdown")]
public static extern int NvmlShutdown(); public static extern int NvmlShutdown();
// Get device count
/* [DllImport("nvml.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int nvmlDeviceGetCount_v2(ref uint deviceCount); */
/* public static int NvmlDeviceGetCount(ref uint deviceCount)
{
return nvmlDeviceGetCount_v2(ref deviceCount);
} */
[DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetHandleByIndex_v2")] [DllImport("nvml.dll", EntryPoint = "nvmlDeviceGetHandleByIndex_v2")]
public static extern int NvmlDeviceGetHandleByIndex(int index, out IntPtr device); public static extern int NvmlDeviceGetHandleByIndex(int index, out IntPtr device);
@@ -21,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;
}
} }
+51 -2
View File
@@ -1,5 +1,10 @@
using Microsoft.AspNetCore.Hosting;
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 +13,64 @@ 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()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
// URLs are now configured via appsettings.{Environment}.json files
})
.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;
+562 -75
View File
@@ -1,107 +1,594 @@
# Resource Usage API # Resource Monitor Service
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 with a modern web dashboard for real-time resource monitoring. Originally designed for Windows VMs running on Unraid servers, now featuring a responsive web interface for easy monitoring and management.
## Features ## 🌟 New: Web Dashboard
- **Resource Monitoring**: Fetches detailed information about the system's resources, including: Access the interactive web dashboard:
- Current Time - **Development**: `http://localhost:5000`
- Computer Information (Machine Name, OS Version, Architecture, Processor Count) - **Release/Production**: `http://localhost:24142`
- CPU Usage
- RAM Usage
- GPU Usage
- Currently Running Steam Games (if any)
- **Process Management**: Provides an API to kill processes by their process ID. Features:
## Directory Structure - **Real-time Monitoring**: Live updates every 15 seconds via SignalR
- **Responsive Design**: Mobile-friendly interface built with Tailwind CSS
- **Interactive Controls**: Toggle auto-refresh, show/hide sections, manual refresh
- **Game Detection**: Prominent game monitoring with process termination
- **Process Management**: View and terminate top processes
- **System Details**: Comprehensive system information and disk usage
- **Performance Charts**: Historical CPU and memory usage graphs
- **API Documentation**: Built-in Swagger/OpenAPI interface at `/swagger`
## 🚀 Features
### Web Dashboard
- **Modern Interface**: Clean, responsive design with dark/light themes
- **Real-time Updates**: SignalR-powered live data updates
- **Auto-refresh Control**: Toggle between automatic and manual refresh modes
- **Game Detection Section**: Monitor running games with termination capability
- **Process Management**: View top processes with one-click termination
- **System Information**: Detailed hardware and software information
- **Disk Usage Visualization**: Visual disk space utilization
- **Performance Charts**: Historical data visualization with Chart.js
- **Mobile Responsive**: Works seamlessly on phones, tablets, and desktop
### Core Monitoring
- **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection
- **Memory Monitoring**: RAM usage, available memory, committed memory, and paging
- **GPU Monitoring**: NVIDIA GPU usage, memory utilization, temperature, fan speed, and power consumption (via NVML)
- **Disk Monitoring**: I/O statistics, space usage, and performance counters
- **Network Monitoring**: Bandwidth usage, packet statistics, and interface data
- **Temperature Monitoring**: CPU and hard drive temperature sensors
### VM-Specific Features
- **VM Detection**: Automatically detects virtualization environment
- **Hypervisor Identification**: Identifies VMware, VirtualBox, Hyper-V, KVM, etc.
- **Unraid Optimization**: Optimized for Unraid VM environments
- **Resource Alerting**: Configurable thresholds for resource usage alerts
### Advanced Features
- **Game Detection**: Multi-platform game detection with fullscreen monitoring 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 with timer support (hidden feature)
### 🔐 Hidden System Control Feature
For security reasons, the system control feature is hidden by default. To access it:
**Mobile Device Access Methods:**
1. **Long press** (2 seconds) on the "Resource Monitor" title in the navigation bar
2. **Triple tap** quickly on the "Resource Monitor" title
3. **Five quick taps** on the chart icon (📊) next to the title
**Desktop Access Methods:**
1. **Triple-click** on the "Resource Monitor" title in the navigation bar
2. **Keyboard shortcut**: `Ctrl + Shift + P`
3. **Long press** (2 seconds) on the title with mouse
**Features:**
- **Shutdown**: Graceful or forced system shutdown
- **Restart**: System restart with optional force
- **Timer Support**: Delay execution from 0 to 86400 seconds (24 hours) - Default: 15 seconds
- **Cancel Function**: Cancel any pending shutdown/restart (similar to `shutdown -a`)
- **Safety Confirmations**: Multiple confirmation dialogs before execution
- **Auto-lock**: System control auto-locks after 30 seconds for security
**Usage:**
1. Activate the system control button using one of the methods above
2. Look for the red pulsing "System" button that appears in the navigation
3. Tap/click the "System" button to open the control panel
4. Adjust timer if needed (defaults to 15 seconds for safety)
5. Choose shutdown, restart, or cancel any pending operation
6. Confirm the action
📱 **Mobile Tips:**
- Device will vibrate when system control is unlocked (if supported)
- Touch-friendly interface optimized for mobile screens
- All gestures work with touchscreens
⚠️ **Warning**: Use this feature with extreme caution as it will shut down or restart the entire system.
- **Health Monitoring**: Comprehensive health checks and uptime tracking
- **Real-time Metrics**: CPU usage calculation and memory percentage tracking for processes
## 📡 API Endpoints
The service runs a web server providing:
- **Development**: `http://localhost:5000`
- **Release/Production**: `http://localhost:24142`
Both environments provide:
### Web Interface
- `GET /` - **Main Dashboard** - Interactive web interface for monitoring
- `GET /swagger` - **API Documentation** - Interactive API explorer and documentation
### REST API
All API endpoints are available at:
- **Development**: `http://localhost:5000/api/[endpoint]`
- **Release/Production**: `http://localhost:24142/api/[endpoint]`
Endpoints:
### System Information
- `GET /api/resource/usage` - **Complete resource overview** - All monitoring data in one call
- `GET /api/resource/system-info` - Complete system information including VM details
- `GET /api/resource/cpu` - Detailed CPU metrics with per-core data
- `GET /api/resource/memory` - Memory utilization and statistics
- `GET /api/resource/gpu` - NVIDIA GPU usage, memory, temperature, fan speed, and power consumption
- `GET /api/resource/disks` - Disk I/O and space usage for all drives
- `GET /api/resource/network` - Network interface statistics
- `GET /api/resource/processes?count=10` - Top processes by CPU/memory usage with percentage data
### Process Management
- `POST /api/resource/kill-process/{processId}` - **Terminate Process** - End any process by ID
### Real-time Updates
- **SignalR Hub**: `/resourceHub` - Real-time data updates every 15 seconds
### 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: Web Dashboard (Recommended)
```powershell
cd C:\Work\DEV\ResourceUsageAPI
dotnet run --configuration Release
``` ```
ResourceUsageAPI/ Then open your browser to:
├── Worker.cs - Development: `http://localhost:5000`
├── Program.cs - Release/Production: `http://localhost:24142`
├── Startup.cs
└── NvmlWrapper.cs for the interactive dashboard.
### Option 2: Windows Service (Production)
```powershell
# Run as Administrator
cd C:\Work\DEV\ResourceUsageAPI
.\install-service.ps1
``` ```
## Code Analysis ### Option 3: Linux Service (if running on Linux)
```bash
cd /path/to/ResourceUsageAPI
chmod +x install-service.sh
sudo ./install-service.sh
```
### Worker.cs ### Option 4: Standalone Executable
```powershell
cd C:\Work\DEV\ResourceUsageAPI
dotnet build --configuration Release
dotnet publish --configuration Release
cd bin\Release\net9.0-windows\publish
.\ResourceMonitorService.exe
```
This file contains the main logic for monitoring system resources and exposing APIs. ## 🎮 Web Dashboard Features
- **Dependencies**: Uses `System.Diagnostics`, `Microsoft.AspNetCore.Builder`, `Newtonsoft.Json` among others. ### Dashboard Overview
- **Methods**: - **Resource Cards**: CPU, Memory, GPU, and Network usage with visual progress bars
- `ExecuteAsync`: Sets up the ASP.NET Core web application, defines routes for resource usage and process management, and runs the server. - **Game Detection**: Prominent section showing currently running games
- `GetComputerInfo`: Retrieves basic system information. - **Auto-refresh Toggle**: Control automatic updates (15-second intervals)
- `GetCpuUsage`: Fetches CPU usage and lists top three processes by CPU usage if usage is over 80%. - **Manual Refresh**: Force immediate data updates
- `GetRamUsage`: Calculates RAM usage percentage. - **Responsive Design**: Works on desktop, tablet, and mobile devices
- `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 ### Interactive Sections
- **Processes**: View and terminate top CPU/memory consuming processes
- **Details**: System information, disk usage, and performance charts
- **Game Management**: Monitor and terminate running games
- **Real-time Charts**: Historical CPU and memory usage visualization
This file sets up the hosting environment for the application. ### Controls
- **Auto: ON/OFF** - Toggle automatic data updates
- **Processes** - Show/hide process management table
- **Details** - Show/hide system information and charts
- **Refresh** - Manually update all data immediately
- **Dependencies**: Uses `Microsoft.Extensions.DependencyInjection` and `Microsoft.Extensions.Hosting`. ## 📊 Service Management
- **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 ### Starting and Stopping the Service
This file is not used in the current implementation since all routing and configuration are done within `Worker.cs`. ```powershell
# Start the service
Start-Service "ResourceMonitorService"
- **Dependencies**: Uses `Microsoft.AspNetCore.Builder` and `Microsoft.AspNetCore.Hosting`. # Stop the service
- **Methods**: Stop-Service "ResourceMonitorService"
- `ConfigureServices`: Placeholder method for adding services.
- `Configure`: Placeholder method for configuring application HTTP requests pipeline.
### NvmlWrapper.cs # Get service status
Get-Service "ResourceMonitorService"
This file provides a C# wrapper for the NVIDIA Management Library (NVML) functions. # Restart the service
Restart-Service "ResourceMonitorService"
```
- **Dependencies**: Uses `System` and `System.Runtime.InteropServices`. ### Development Mode
- **Methods**:
- Importing NVML DLL functions to interact with GPU hardware.
- Structures like `NvmlUtilization` are defined for handling utilization rates returned by NVML.
## Usage For development and testing:
1. **Build the Project**: Use your preferred .NET build tool (e.g., `dotnet build`) to compile the project. ```powershell
2. **Run the Application**: # Run in development mode with hot reload
- To run as a console application, execute the compiled binary directly. dotnet run --environment Development
- To run as a Windows service, use the command-line argument `--windows-service` or set the environment variable `RUN_AS_SERVICE` to `"true"`.
## APIs # Access the dashboard at:
# - Development: http://localhost:5000
# - Release/Production: http://localhost:24142
# Access Swagger API documentation at the same URL + /swagger
```
- **Get Resource Usage**: ### Troubleshooting
- URL: `/api/resource-usage`
- Method: GET
- Description: Retrieves current system resource usage.
- **Kill Process**: - **Service won't start**: Check the logs in the `logs/` directory
- URL: `/api/kill-process` - **No GPU data**: Make sure you have an NVIDIA GPU and drivers installed
- Method: POST - **High CPU usage**: Adjust monitoring intervals in `appsettings.json`
- Body: JSON with the process ID (`{"id": "1234"}`) - **Web dashboard not accessible**: Verify firewall settings and ensure the appropriate port is available (5000 for development, 24142 for release/production)
- Description: Kills the specified process. - **Game detection issues**: Check if games are running from standard installation directories
- **API errors**: Verify endpoints using Swagger documentation at `/swagger`
- **Performance issues**: Consider increasing `UpdateIntervalMs` in configuration
## Important Notes ## ⚙️ Configuration
- Ensure that the NVIDIA Management Library (NVML) is installed on the system for GPU monitoring to work. Configuration is managed through `appsettings.json`:
- The application allows CORS from all origins, which should be configured securely in production environments.
- Error handling and logging are minimal; consider adding robust error handling and logging mechanisms for a production-ready solution.
## Contributing ```json
{
"MonitoringSettings": {
"UpdateIntervalMs": 15000,
"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"
]
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
},
"Https": {
"Url": "https://localhost:5001"
}
}
}
}
```
Feel free to contribute by opening issues or submitting pull requests. Make sure to follow the project's coding style and best practices. ### Game Detection Configuration
The service supports advanced game detection through two complementary approaches:
# devnote #### **Platform-Based Detection**
dotnet run Automatically detects games installed through popular game platforms:
git add . - **Steam**: Games in `\steamapps\common\` directories
git commit -m "Add steam running games" - **Epic Games Store**: Games in `\Epic Games\` directories
git push origin master - **GOG Galaxy**: Games in `\GOG Galaxy\Games\` directories
dotnet publish -c Release -o ./publish - **EA Origin**: Games in `\Origin Games\` directories
- **Ubisoft Connect**: Games in `\Ubisoft Game Launcher\games\` directories
#### **Root Folder Detection**
Configure custom game directories for standalone games and non-platform installations:
```json
"GameRootFolders": [
"C:\\Games",
"D:\\Games",
"E:\\Games"
]
```
**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
**Example Game Detection:**
```
C:\Games\Cyberpunk 2077\bin\x64\Cyberpunk2077.exe
→ Game Name: "Cyberpunk 2077"
→ Platform: "Standalone"
D:\Games\The Witcher 3\witcher3.exe
→ Game Name: "The Witcher 3"
→ Platform: "Standalone"
```
**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
### Telegram Bot Alerts
The service supports real-time alert notifications via Telegram bot. To set up Telegram alerts:
1. **Create a Telegram Bot** - Contact `@BotFather` on Telegram and create a new bot
2. **Get Chat ID** - Send a message to your bot, then visit `https://api.telegram.org/bot<TOKEN>/getUpdates`
3. **Configure Settings** - Add Telegram configuration to your `appsettings.json`:
```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}"
}
}
}
```
**Features:**
- **Multiple Chats**: Send alerts to multiple users/groups by adding chat IDs
- **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
📋 For detailed setup instructions, see [TELEGRAM_SETUP.md](TELEGRAM_SETUP.md)
## 📊 Example API Responses
### Health Check
```json
{
"status": "Healthy",
"timestamp": "2025-08-07T02:30:00Z",
"uptime": "1.16:55:30",
"activeAlerts": 0,
"monitoringEnabled": {
"gpu": true,
"disk": true,
"network": true,
"temperature": true,
"processes": true,
"games": true,
"alerts": true
}
}
```
### CPU Usage
```json
{
"usage": 15.5,
"coreUsages": [12.1, 18.3, 14.7, 16.2],
"temperature": 65.0,
"maxFrequency": 4400,
"currentFrequency": 3200,
"isThrottling": false
}
```
### VM Information
```json
{
"isVirtualMachine": true,
"hypervisorVendor": "VMware",
"uptime": "1.16:55:30",
"bootTime": "2025-08-05T09:34:04Z",
"machineName": "WIN11-VM",
"domain": "WORKGROUP"
}
```
### 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": ""
}
```
### Game Detection
```json
{
"gameName": "Cyberpunk 2077",
"executableName": "Cyberpunk2077.exe",
"fullPath": "C:\\Games\\Cyberpunk 2077\\bin\\x64\\Cyberpunk2077.exe",
"processId": 8432,
"memoryUsage": 4294967296,
"cpuTime": "00:15:42.1250000",
"startTime": "2025-08-07T14:30:15.123456+08:00",
"platform": "Standalone",
"isFullscreen": true,
"fps": 0
}
```
### 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
# Access the web dashboard (adjust port based on environment)
# Development:
Start-Process "http://localhost:5000"
# Release/Production:
Start-Process "http://localhost:24142"
# Get complete resource overview (adjust URL as needed)
$baseUrl = "http://localhost:24142" # Use 5000 for development
$resources = Invoke-RestMethod -Uri "$baseUrl/api/resource/usage"
Write-Host "CPU: $($resources.cpu.usage.ToString('F1'))%"
Write-Host "Memory: $($resources.memory.usagePercentage.ToString('F1'))%"
Write-Host "GPU: $($resources.gpu.usage)%"
# Get system information
$systemInfo = Invoke-RestMethod -Uri "$baseUrl/api/resource/system-info"
Write-Host "Machine: $($systemInfo.machineName)"
Write-Host "OS: $($systemInfo.osVersion)"
Write-Host "CPU: $($systemInfo.cpuName)"
# Get disk usage
$disks = Invoke-RestMethod -Uri "$baseUrl/api/resource/disks"
foreach ($disk in $disks) {
$freeGB = [math]::Round($disk.freeSpace / 1GB, 1)
$totalGB = [math]::Round($disk.totalSize / 1GB, 1)
Write-Host "$($disk.driveLetter): $freeGB GB free of $totalGB GB"
}
# Get top processes
$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/resource/processes?count=5"
Write-Host "Top 5 processes by CPU usage:"
foreach ($proc in $processes) {
if ($proc.cpuUsage) {
Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F1'))% CPU"
}
}
# Terminate a process (example - be careful!)
# Invoke-RestMethod -Uri "http://localhost:5000/api/resource/kill-process/1234" -Method Post
```
## 🚨 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 (15-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
+146
View File
@@ -0,0 +1,146 @@
# Resource Monitor Web UI
This enhanced Resource Monitor Service now includes a modern web-based dashboard with REST API functionality.
## Features
### Web Dashboard
- **Mobile-Friendly Design**: Built with Tailwind CSS for responsive design
- **Real-Time Updates**: Uses SignalR for live data updates every 5 seconds
- **Interactive Dashboard**: Shows CPU, Memory, GPU, and Network usage with progress bars
- **Process Management**: View top 10 processes with ability to kill top 3 high-usage processes
- **Detailed View**: Toggle detailed system information and disk usage
- **Performance Charts**: Real-time CPU and Memory usage history
### REST API Endpoints
#### Resource Information
- `GET /api/resource/usage` - Get complete resource usage information
- `GET /api/resource/system-info` - Get system information
- `GET /api/resource/cpu` - Get CPU usage details
- `GET /api/resource/memory` - Get memory usage details
- `GET /api/resource/gpu` - Get GPU usage details
- `GET /api/resource/disks` - Get disk usage for all drives
- `GET /api/resource/network` - Get network usage details
- `GET /api/resource/processes?count=10` - Get top processes
#### Process Management
- `POST /api/resource/kill-process/{processId}` - Terminate a specific process
#### API Documentation
- `GET /swagger` - Swagger UI for API documentation (in Development mode)
## Running the Application
### Development Mode
```bash
dotnet run
```
The application will be available at:
- Web UI: http://localhost:5000
- API: http://localhost:5000/api/
- Swagger UI: http://localhost:5000/swagger
### Windows Service Mode
```bash
dotnet run --windows-service
```
### Build and Deploy
```bash
dotnet build --configuration Release
dotnet publish --configuration Release --output ./publish
```
## Configuration
### Port Configuration
Edit `appsettings.json` to change ports:
```json
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
},
"Https": {
"Url": "https://*:5001"
}
}
}
}
```
### Monitoring Settings
Configure monitoring intervals and features in `appsettings.json`:
```json
{
"MonitoringSettings": {
"UpdateIntervalMs": 5000,
"EnableGpuMonitoring": true,
"EnableProcessMonitoring": true,
"MaxProcessesToTrack": 10
}
}
```
## Security Features
### Process Kill Restrictions
- Only the top 3 highest CPU/Memory usage processes can be terminated
- Confirmation dialog required for process termination
- All process terminations are logged
### API Security
- CORS configured for allowed origins
- Optional API key authentication (disabled by default)
- Rate limiting can be configured
## Dashboard Features
### Main Dashboard Cards
1. **CPU Usage**: Real-time percentage with progress bar
2. **Memory Usage**: Memory utilization with progress bar
3. **GPU Usage**: Graphics processor utilization
4. **Network**: Upload/download speeds
### Process Table
- Shows top 10 processes by CPU/Memory usage
- Process ID, Name, CPU%, Memory usage
- Kill button available for top 3 processes only
### Detailed Information (Toggle)
- Complete system information
- Disk usage for all drives
- Real-time performance charts
- Historical CPU and Memory usage graphs
### Mobile Responsive
- Optimized for mobile devices
- Touch-friendly interface
- Responsive grid layout
- Collapsible sections
## Technology Stack
- **Backend**: ASP.NET Core 9.0
- **Frontend**: HTML5, Tailwind CSS, Chart.js
- **Real-time**: SignalR
- **API Documentation**: Swagger/OpenAPI
- **Icons**: Font Awesome
- **Charts**: Chart.js
## Browser Compatibility
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
## Development Notes
The application runs both as a web server and a Windows service background worker simultaneously, providing:
- Web interface for interactive monitoring
- REST API for programmatic access
- Background service for continuous monitoring
- Real-time updates via WebSocket (SignalR)
+9 -2
View File
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Worker"> <Project Sdk="Microsoft.NET.Sdk.Web">
<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,11 @@
<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" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.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);
}
}
}
+555
View File
@@ -0,0 +1,555 @@
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"
};
// Exclude gaming platform clients (they are not games themselves)
var platformClientExclusions = new[]
{
"steam", "steamwebhelper", "steamservice", "steamerrorreporter",
"epicgameslauncher", "epic games launcher", "epiconlineservices",
"origin", "originwebhelperservice", "originweb", "originthinsetup",
"uplay", "upc", "ubisoftgamelauncher", "ubisoftconnect",
"battle.net", "battlenet", "blizzardlauncher", "blizzard update agent",
"gog galaxy", "goggalaxy", "gogcom",
"rockstar games launcher", "rockstargameslauncher",
"riotclientservices", "riot client", "valorant-win64-shipping",
"ea app", "ea desktop", "eadesktop", "eabackground",
"xbox", "xboxapp", "xboxgamebar", "microsoftstore"
};
// Skip if it's a known system process or platform client
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)) ||
platformClientExclusions.Any(exclusion => fileName.Contains(exclusion)))
{
return null;
}
var knownGameExecutables = new[]
{
"game"
// Removed "launcher" and "client" as they often refer to platform clients, not games
};
var gameIndicators = new[]
{
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
"\\steamapps\\common\\", "\\epic games\\", "\\gog galaxy\\games\\",
"\\origin games\\", "\\ubisoft game launcher\\games\\"
};
// 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))
{
// Check if this is a platform client by product name
var platformClientNames = new[]
{
"steam", "epic games launcher", "origin", "uplay", "ubisoft connect",
"battle.net", "blizzard launcher", "gog galaxy", "riot client",
"ea app", "ea desktop", "xbox", "microsoft store", "rockstar games launcher"
};
if (platformClientNames.Any(client =>
versionInfo.ProductName.Contains(client, StringComparison.OrdinalIgnoreCase)))
{
return string.Empty; // Return empty to indicate this should not be treated as a game
}
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;
}
}
}
}
+224
View File
@@ -0,0 +1,224 @@
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;
}
// Ignore alerts from svchost processes
if (alert.Component.Contains("svchost", StringComparison.OrdinalIgnoreCase) ||
alert.Message.Contains("svchost", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Skipping Telegram alert for svchost process: {Component} - {Message}",
alert.Component, alert.Message);
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;
// Ignore alerts from svchost processes
if (alert.Component.Contains("svchost", StringComparison.OrdinalIgnoreCase) ||
alert.Message.Contains("svchost", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Skipping Telegram resolution notification for svchost process: {Component} - {Message}",
alert.Component, alert.Message);
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("!", "\\!");
}
}
}
+56 -15
View File
@@ -3,29 +3,70 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using ResourceMonitorService.Hubs;
using Microsoft.OpenApi.Models;
public class Startup namespace ResourceMonitorService
{ {
public void ConfigureServices(IServiceCollection services) public class Startup
{ {
// Add services to the container public void ConfigureServices(IServiceCollection services)
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); // Add MVC services
services.AddControllers()
.AddNewtonsoftJson(); // For JSON serialization
// Add SignalR for real-time updates
services.AddSignalR();
// Add CORS for API access
services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Add Swagger for API documentation
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Resource Monitor API", Version = "v1" });
});
} }
app.UseRouting(); public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseEndpoints(endpoints =>
{ {
endpoints.MapGet("/", async context => if (env.IsDevelopment())
{ {
await context.Response.WriteAsync("Hello World!"); app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resource Monitor API V1");
});
}
// Serve static files (CSS, JS, images)
app.UseStaticFiles();
app.UseRouting();
app.UseCors("AllowAll");
app.UseEndpoints(endpoints =>
{
// Map API controllers
endpoints.MapControllers();
// Map SignalR hub
endpoints.MapHub<ResourceHub>("/resourceHub");
// Default route to index.html
endpoints.MapFallbackToFile("index.html");
}); });
}); }
} }
} }
+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
```
+106 -231
View File
@@ -1,262 +1,137 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Newtonsoft.Json; using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options;
using System.Runtime.InteropServices; using ResourceMonitorService.Configuration;
using System.Management; using ResourceMonitorService.Services;
using ResourceMonitorService.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace ResourceMonitorService namespace ResourceMonitorService
{ {
public class Worker : BackgroundService public class Worker : BackgroundService
{ {
private readonly IHostApplicationLifetime _lifetime; private readonly ILogger<Worker> _logger;
private readonly IResourceMonitorService _resourceMonitorService;
private readonly IGameDetectionService _gameDetectionService;
private readonly IAlertService _alertService;
private readonly MonitoringSettings _monitoringSettings;
private readonly IServiceProvider _serviceProvider;
public Worker(IHostApplicationLifetime lifetime) public Worker(
ILogger<Worker> logger,
IResourceMonitorService resourceMonitorService,
IGameDetectionService gameDetectionService,
IAlertService alertService,
IOptions<MonitoringSettings> monitoringSettings,
IServiceProvider serviceProvider)
{ {
_lifetime = lifetime; _logger = logger;
_resourceMonitorService = resourceMonitorService;
_gameDetectionService = gameDetectionService;
_alertService = alertService;
_monitoringSettings = monitoringSettings.Value;
_serviceProvider = serviceProvider;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
var builder = WebApplication.CreateBuilder(); _logger.LogInformation("Resource Monitor background service starting...");
builder.Services.AddCors(options => await BackgroundMonitoringLoop(stoppingToken);
{
options.AddPolicy("AllowAllOrigins",
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
builder.Services.AddControllers().AddNewtonsoftJson();
var app = builder.Build();
// Apply CORS policy to allow all origins
app.UseCors("AllowAllOrigins");
app.MapGet("/api/resource-usage", async context =>
{
var currentTime = GetCurrentTime();
var computerInfo = GetComputerInfo();
var cpuUsage = GetCpuUsage();
var ramUsage = GetRamUsage();
var gpuUsage = GetGpuUsage();
var runningGame = GetCurrentlyRunningGame();
var resourceUsage = new
{
CurrentTime = currentTime,
ComputerInfo = computerInfo,
CPU = cpuUsage,
RAM = ramUsage,
GPU = gpuUsage,
CurrentlyRunningGame = runningGame
};
var json = JsonConvert.SerializeObject(resourceUsage);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(json);
});
app.MapPost("/api/kill-process", async context =>
{
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();
if (processes.Length > 0)
{
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}");
}
});
_ = app.RunAsync(stoppingToken);
await Task.Delay(Timeout.Infinite, stoppingToken);
} }
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
var processes = Process.GetProcesses().OrderByDescending(p => p.TotalProcessorTime);
// 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()
{
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
};
}
private object GetCurrentlyRunningGame()
{
var processes = Process.GetProcesses();
foreach (var process in processes)
{ {
try try
{ {
#pragma warning disable CS8602 // Dereference of a possibly null reference. // Get current resource usage
var filePath = process.MainModule.FileName; var resourceUsage = await _resourceMonitorService.GetResourceUsageAsync();
#pragma warning restore CS8602 // Dereference of a possibly null reference.
if (filePath.Contains(@"\steamapps\common\")) // Add current game info if game detection is enabled
if (_monitoringSettings.EnableGameDetection)
{ {
// Extract the game directory name try
var parts = filePath.Split(new[] { @"\steamapps\common\" }, StringSplitOptions.None);
if (parts.Length > 1)
{ {
var gamePath = parts[1]; resourceUsage.RunningGame = await _gameDetectionService.GetCurrentlyRunningGameAsync();
var gameName = gamePath.Split(Path.DirectorySeparatorChar)[0]; }
return new catch (Exception ex)
{
// Only log game detection errors occasionally to avoid spam
if (errorCount % 12 == 0) // Every minute if 5-second intervals
{ {
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);
}
// Send real-time updates via SignalR
try
{
using var scope = _serviceProvider.CreateScope();
var hubContext = scope.ServiceProvider.GetService<IHubContext<ResourceHub>>();
if (hubContext != null)
{
await hubContext.Clients.All.SendAsync("ResourceUpdate", resourceUsage, cancellationToken);
}
}
catch (Exception ex)
{
_logger.LogDebug("SignalR broadcast error: {Message}", ex.Message);
}
successfulCycles++;
// Log performance metrics occasionally
if (successfulCycles % 4 == 0) // Every 60 seconds with 15-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);
}
} }
} }
+10
View File
@@ -4,5 +4,15 @@
"Default": "Information", "Default": "Information",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
},
"Https": {
"Url": "https://*:5001"
}
}
} }
} }
+15
View File
@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:24142"
}
}
}
}
+95 -5
View File
@@ -2,15 +2,105 @@
"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": { "ApiSettings": {
"Endpoints": { "ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a",
"Http": { "RequireApiKey": false,
"Url": "http://*:5000" "AllowedOrigins": [
"http://localhost:4200",
"http://192.168.50.52:4200",
"http://vmwin11:4200",
"http://unraid:4200"
],
"EnableSwagger": false,
"BasePath": "/api"
},
"MonitoringSettings": {
"UpdateIntervalMs": 60000,
"DataRetentionDays": 7,
"EnableGpuMonitoring": true,
"EnableDiskMonitoring": true,
"EnableNetworkMonitoring": true,
"EnableTemperatureMonitoring": true,
"EnableProcessMonitoring": true,
"EnableDetailedCpuCoreMonitoring": false,
"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}"
}
}
}
+51
View File
@@ -0,0 +1,51 @@
# Resource Monitor Service - Complete Build and Deploy Script
# Builds, packages, and optionally deploys to VM in one step
param(
[string]$VMAddress,
[string]$Version = "2.1.0",
[switch]$DeployToVM,
[switch]$UseWinRM,
[switch]$CopyOnly
)
Write-Host "=== Resource Monitor Service - Build & Deploy ===" -ForegroundColor Cyan
Write-Host
# Step 1: Package the release
Write-Host "Step 1: Building and packaging release..." -ForegroundColor Green
try {
& ".\package-release.ps1" -Version $Version
if ($LASTEXITCODE -ne 0) {
throw "Packaging failed"
}
} catch {
Write-Host "✗ Packaging failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Step 2: Deploy if requested
if ($DeployToVM -and $VMAddress) {
Write-Host
Write-Host "Step 2: Deploying to VM..." -ForegroundColor Green
$deployArgs = @("-VMAddress", $VMAddress)
if ($UseWinRM) { $deployArgs += "-UseWinRM" }
if ($CopyOnly) { $deployArgs += "-CopyOnly" }
try {
& ".\deploy-to-vm.ps1" @deployArgs
} catch {
Write-Host "✗ Deployment failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
} else {
Write-Host
Write-Host "Package created successfully!" -ForegroundColor Green
Write-Host "To deploy to VM, run:" -ForegroundColor Yellow
Write-Host " .\deploy-to-vm.ps1 -VMAddress YOUR_VM_IP" -ForegroundColor Cyan
Write-Host " .\deploy-to-vm.ps1 -VMAddress YOUR_VM_IP -UseWinRM" -ForegroundColor Cyan
}
Write-Host
Write-Host "=== Build & Deploy Complete ===" -ForegroundColor Cyan
+54
View File
@@ -0,0 +1,54 @@
# Simple Release Packaging Script
param(
[string]$Version = "2.1.0"
)
$PACKAGE_NAME = "ResourceMonitorService-v$Version-$(Get-Date -Format 'yyyyMMdd-HHmm')"
$TEMP_PATH = ".\temp-release"
$OUTPUT_PATH = ".\release-packages"
Write-Host "Creating release package: $PACKAGE_NAME" -ForegroundColor Green
# Clean up
if (Test-Path $TEMP_PATH) { Remove-Item $TEMP_PATH -Recurse -Force }
if (-not (Test-Path $OUTPUT_PATH)) { New-Item -ItemType Directory -Path $OUTPUT_PATH -Force | Out-Null }
New-Item -ItemType Directory -Path $TEMP_PATH -Force | Out-Null
# Build release
Write-Host "Building release..." -ForegroundColor Yellow
dotnet publish --configuration Release --output $TEMP_PATH --verbosity minimal
# Copy additional files
Start-Sleep -Seconds 2
Copy-Item "install-service.ps1" $TEMP_PATH
Copy-Item "start-service.bat" $TEMP_PATH
Copy-Item "appsettings.json" $TEMP_PATH
Copy-Item "appsettings.Production.json" $TEMP_PATH -ErrorAction SilentlyContinue
Copy-Item "README.md" $TEMP_PATH -ErrorAction SilentlyContinue
# Create deployment readme
@"
# ResourceMonitorService v$Version Deployment
## Installation
1. Extract ZIP to temporary folder
2. Run PowerShell as Administrator
3. Execute: .\install-service.ps1
## Access
- Web Dashboard: http://localhost:24142
- API Health: http://localhost:24142/api/health
Generated: $(Get-Date)
"@ | Out-File "$TEMP_PATH\DEPLOYMENT.txt" -Encoding UTF8
# Create ZIP
Start-Sleep -Seconds 2
$zipPath = "$OUTPUT_PATH\$PACKAGE_NAME.zip"
Compress-Archive -Path "$TEMP_PATH\*" -DestinationPath $zipPath -Force
# Clean up
Remove-Item $TEMP_PATH -Recurse -Force
$size = [math]::Round((Get-Item $zipPath).Length / 1MB, 2)
Write-Host "Package created: $zipPath ($size MB)" -ForegroundColor Green
+138
View File
@@ -0,0 +1,138 @@
# Resource Monitor Service - Remote VM Deployment Script
# Deploys the packaged service to a remote VM
param(
[Parameter(Mandatory=$true)]
[string]$VMAddress,
[Parameter(Mandatory=$false)]
[string]$Username,
[Parameter(Mandatory=$false)]
[string]$PackagePath,
[switch]$UseWinRM,
[switch]$CopyOnly
)
Write-Host "=== Resource Monitor Service - Remote VM Deployment ===" -ForegroundColor Cyan
Write-Host
# Find the latest package if not specified
if (-not $PackagePath) {
$latestPackage = Get-ChildItem -Path ".\release-packages\ResourceMonitorService-v*.zip" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latestPackage) {
$PackagePath = $latestPackage.FullName
Write-Host "Using latest package: $($latestPackage.Name)" -ForegroundColor Green
} else {
Write-Host "✗ No packages found. Run .\package-release.ps1 first" -ForegroundColor Red
exit 1
}
}
if (-not (Test-Path $PackagePath)) {
Write-Host "✗ Package not found: $PackagePath" -ForegroundColor Red
exit 1
}
$packageName = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)
Write-Host "Package: $packageName" -ForegroundColor White
Write-Host "Target VM: $VMAddress" -ForegroundColor White
Write-Host
if ($UseWinRM) {
Write-Host "Using WinRM for deployment..." -ForegroundColor Yellow
# Test WinRM connection
Write-Host "Testing WinRM connection to $VMAddress..." -ForegroundColor Yellow
try {
$session = New-PSSession -ComputerName $VMAddress -Credential (Get-Credential -Message "Enter credentials for $VMAddress")
Write-Host "✓ WinRM connection successful" -ForegroundColor Green
} catch {
Write-Host "✗ WinRM connection failed: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Make sure WinRM is enabled on the target VM:" -ForegroundColor Yellow
Write-Host " winrm quickconfig" -ForegroundColor Gray
Write-Host " Enable-PSRemoting -Force" -ForegroundColor Gray
exit 1
}
# Copy package to VM
Write-Host "Copying package to VM..." -ForegroundColor Yellow
try {
$remoteTemp = "C:\Temp\$packageName"
Invoke-Command -Session $session -ScriptBlock {
param($remotePath)
if (Test-Path $remotePath) { Remove-Item $remotePath -Recurse -Force }
New-Item -ItemType Directory -Path $remotePath -Force | Out-Null
} -ArgumentList $remoteTemp
Copy-Item -Path $PackagePath -Destination "$remoteTemp.zip" -ToSession $session
Write-Host "✓ Package copied to $remoteTemp.zip" -ForegroundColor Green
} catch {
Write-Host "✗ Failed to copy package: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
if (-not $CopyOnly) {
# Extract and install on VM
Write-Host "Extracting and installing on VM..." -ForegroundColor Yellow
try {
$installResult = Invoke-Command -Session $session -ScriptBlock {
param($remotePath, $packageName)
# Extract package
Expand-Archive -Path "$remotePath.zip" -DestinationPath $remotePath -Force
Set-Location $remotePath
# Run installation
$installOutput = & ".\install-service.ps1" 2>&1
return @{
Success = $LASTEXITCODE -eq 0
Output = $installOutput -join "`n"
ExitCode = $LASTEXITCODE
}
} -ArgumentList $remoteTemp, $packageName
if ($installResult.Success) {
Write-Host "✓ Installation completed successfully!" -ForegroundColor Green
Write-Host "Web Dashboard: http://$VMAddress:5000" -ForegroundColor Cyan
} else {
Write-Host "✗ Installation failed (Exit Code: $($installResult.ExitCode))" -ForegroundColor Red
Write-Host "Output:" -ForegroundColor Yellow
Write-Host $installResult.Output -ForegroundColor Gray
}
} catch {
Write-Host "✗ Installation failed: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Clean up session
Remove-PSSession $session
} else {
# Manual deployment instructions
Write-Host "Manual Deployment Instructions:" -ForegroundColor Yellow
Write-Host
Write-Host "1. Copy the package to your VM:" -ForegroundColor White
Write-Host " - Use RDP, shared folders, or network copy" -ForegroundColor Gray
Write-Host " - Package location: $PackagePath" -ForegroundColor Gray
Write-Host
Write-Host "2. On the VM, extract the ZIP file to a temporary directory" -ForegroundColor White
Write-Host
Write-Host "3. Open PowerShell as Administrator and navigate to the extracted directory" -ForegroundColor White
Write-Host
Write-Host "4. Run the installation:" -ForegroundColor White
Write-Host " .\install-service.ps1" -ForegroundColor Cyan
Write-Host " OR" -ForegroundColor Gray
Write-Host " .\INSTALL.bat" -ForegroundColor Cyan
Write-Host
Write-Host "5. Access the web dashboard:" -ForegroundColor White
Write-Host " http://$VMAddress:5000" -ForegroundColor Cyan
Write-Host
Write-Host "Alternative deployment methods:" -ForegroundColor Yellow
Write-Host " PowerShell Remoting: .\deploy-to-vm.ps1 -VMAddress $VMAddress -UseWinRM" -ForegroundColor Gray
Write-Host " Copy only: .\deploy-to-vm.ps1 -VMAddress $VMAddress -UseWinRM -CopyOnly" -ForegroundColor Gray
}
Write-Host
Write-Host "=== Deployment Script Complete ===" -ForegroundColor Cyan
+194
View File
@@ -0,0 +1,194 @@
#!/bin/bash
# Resource Monitor Service - Installation Script for Linux systemd service
SERVICE_NAME="resource-monitor"
SERVICE_DISPLAY_NAME="Resource Monitor Service v2.1"
SERVICE_DESCRIPTION="Monitors system resources with web dashboard"
INSTALL_PATH="/opt/resource-monitor"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
echo "=== Resource Monitor Service - Linux systemd Installer ==="
echo
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (use sudo)"
echo "Please run: sudo ./install-service-linux.sh"
exit 1
fi
# Function to uninstall service
uninstall_service() {
echo "Uninstalling Resource Monitor Service..."
# Stop the service
echo "Stopping service..."
systemctl stop $SERVICE_NAME 2>/dev/null
echo "Service stopped"
# Disable the service
echo "Disabling service..."
systemctl disable $SERVICE_NAME 2>/dev/null
echo "Service disabled"
# Remove service file
echo "Removing service file..."
rm -f $SERVICE_FILE
echo "Service file removed"
# Reload systemd
systemctl daemon-reload
# Optionally remove installation directory
read -p "Remove installation files from $INSTALL_PATH? (y/N): " removeFiles
if [[ "$removeFiles" == "y" || "$removeFiles" == "Y" ]]; then
rm -rf $INSTALL_PATH
echo "Installation files removed"
fi
echo "Uninstallation complete!"
exit 0
}
# Check for uninstall flag
if [[ "$1" == "--uninstall" || "$1" == "-u" ]]; then
uninstall_service
fi
echo "Installing Resource Monitor Service as systemd service..."
# Check if .NET is installed
if ! command -v dotnet &> /dev/null; then
echo "ERROR: .NET runtime is not installed"
echo "Please install .NET 8.0 or later runtime"
echo "See: https://docs.microsoft.com/en-us/dotnet/core/install/linux"
exit 1
fi
# Create installation directory
echo "Creating installation directory..."
mkdir -p "$INSTALL_PATH"
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create installation directory"
exit 1
fi
echo "Installation directory created: $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
echo "Service built successfully"
# Stop existing service if running
echo "Stopping existing service (if running)..."
systemctl stop $SERVICE_NAME 2>/dev/null || echo "No existing service found"
# Create systemd service file
echo "Creating systemd service file..."
cat > $SERVICE_FILE << EOF
[Unit]
Description=$SERVICE_DESCRIPTION
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/dotnet $INSTALL_PATH/ResourceMonitorService.dll
Restart=always
RestartSec=5
SyslogIdentifier=resource-monitor
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://localhost:5000
WorkingDirectory=$INSTALL_PATH
[Install]
WantedBy=multi-user.target
EOF
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create service file"
exit 1
fi
echo "Service file created"
# Set proper permissions
chmod 644 $SERVICE_FILE
chown root:root $SERVICE_FILE
# Reload systemd
echo "Reloading systemd daemon..."
systemctl daemon-reload
# Enable the service
echo "Enabling service..."
systemctl enable $SERVICE_NAME
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to enable service"
exit 1
fi
echo "Service enabled"
# Start the service
echo "Starting service..."
systemctl start $SERVICE_NAME
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to start service"
echo "Check service status with: systemctl status $SERVICE_NAME"
exit 1
fi
echo "Service started successfully"
# Wait a moment and check service status
sleep 3
SERVICE_STATUS=$(systemctl is-active $SERVICE_NAME)
echo "Service Status: $SERVICE_STATUS"
# Configure firewall (if ufw is available)
if command -v ufw &> /dev/null; then
echo "Configuring firewall for web dashboard..."
ufw allow 5000/tcp comment "Resource Monitor Service"
echo "Firewall rule created for port 5000"
fi
echo
echo "=== Installation Complete ==="
echo "Service Name: $SERVICE_NAME"
echo "Installation Path: $INSTALL_PATH"
echo "Web Dashboard: http://localhost:5000"
echo "API Documentation: http://localhost:5000/swagger"
echo "API Health Check: http://localhost:5000/api/health"
echo
echo "The service is now running and will start automatically with the system."
echo "You can manage it using systemctl commands:"
echo " - Stop: sudo systemctl stop $SERVICE_NAME"
echo " - Start: sudo systemctl start $SERVICE_NAME"
echo " - Status: systemctl status $SERVICE_NAME"
echo " - Restart: sudo systemctl restart $SERVICE_NAME"
echo " - Logs: journalctl -u $SERVICE_NAME -f"
echo
echo "To uninstall: sudo ./install-service-linux.sh --uninstall"
# Test the web dashboard
echo
echo "Testing web dashboard..."
sleep 5
if command -v curl &> /dev/null; then
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/api/health)
if [[ "$RESPONSE" == "200" ]]; then
echo "Web Dashboard Test: SUCCESS"
echo
echo "🎉 Web Dashboard is ready at: http://localhost:5000"
echo "📖 API Documentation at: http://localhost:5000/swagger"
else
echo "Web Dashboard Test: FAILED (HTTP $RESPONSE)"
echo "The service may still be starting up. Wait a few minutes and try accessing:"
echo "http://localhost:5000"
fi
else
echo "curl not available for testing. Please check manually:"
echo "http://localhost:5000"
fi
+220
View File
@@ -0,0 +1,220 @@
# 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.1"
$SERVICE_DESCRIPTION = "Monitors system resources with web dashboard 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
}
# Check if service is currently running and stop it before building
Write-Host "Checking for existing service..."
try {
$existingService = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
if ($existingService) {
Write-Host "Found existing service: $($existingService.Status)" -ForegroundColor Yellow
if ($existingService.Status -eq "Running") {
Write-Host "Stopping running service before build..."
Stop-Service -Name $SERVICE_NAME -Force -ErrorAction Stop
Write-Host "Service stopped successfully" -ForegroundColor Green
Start-Sleep -Seconds 2 # Give it a moment to fully stop
}
} else {
Write-Host "No existing service found" -ForegroundColor Gray
}
} catch {
Write-Host "Warning: Could not check existing service status: $($_.Exception.Message)" -ForegroundColor Yellow
}
# 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 for web dashboard
Write-Host "Configuring Windows Firewall for web dashboard..."
try {
# Remove old rule if it exists
Remove-NetFirewallRule -DisplayName "Resource Monitor Service" -ErrorAction SilentlyContinue
# Create new rule for port 24142 (web dashboard)
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 24142 -Action Allow -Profile Any -ErrorAction Stop
Write-Host "Firewall rule created for web dashboard (port 24142)" -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 24142 -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 "Web Dashboard: http://localhost:24142" -ForegroundColor Yellow
Write-Host "API Documentation: http://localhost:24142/swagger" -ForegroundColor Yellow
Write-Host "API Health Check: http://localhost:24142/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 web dashboard
Write-Host
Write-Host "Testing web dashboard..." -ForegroundColor Yellow
Start-Sleep -Seconds 5
try {
$response = Invoke-RestMethod -Uri "http://localhost:24142/api/health" -TimeoutSec 10
Write-Host "Web Dashboard Test: SUCCESS" -ForegroundColor Green
Write-Host "Service Status: $($response.status)" -ForegroundColor White
Write-Host "Service Uptime: $($response.uptime)" -ForegroundColor White
Write-Host
Write-Host "Web Dashboard is ready at: http://localhost:24142" -ForegroundColor Green
Write-Host "API Documentation at: http://localhost:24142/swagger" -ForegroundColor Green
} catch {
Write-Host "Web Dashboard Test: 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:24142" -ForegroundColor White
}
View File
-8
View File
@@ -1,8 +0,0 @@
# PowerShell script to create a new inbound rule in Windows Firewall
$port = 5000
$ruleName = "ResourceMonitorService"
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 ResourceMonitorServicerrr binPath= "%~dp0ResourceMonitorService.exe --windows-service" start= auto
sc description ResourceMonitorServicerrr "A service that monitors system resource usage and exposes it via a web API."
+27
View File
@@ -0,0 +1,27 @@
# Quick API Test Command
# Run this after starting the service with: dotnet run
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
try {
$response = Invoke-WebRequest -Uri "http://localhost:5000/api/resource/usage" -UseBasicParsing
$stopwatch.Stop()
$content = $response.Content | ConvertFrom-Json
Write-Host "✅ API Response Time: $($stopwatch.ElapsedMilliseconds)ms" -ForegroundColor Green
Write-Host " Status: $($response.StatusCode)" -ForegroundColor Cyan
Write-Host " CPU Usage: $($content.CPU.Usage)%" -ForegroundColor White
Write-Host " Memory Usage: $($content.Memory.UsagePercentage)%" -ForegroundColor White
Write-Host " Core Count: $($content.CPU.CoreUsages.Length)" -ForegroundColor White
if ($stopwatch.ElapsedMilliseconds -lt 1000) {
Write-Host "🚀 Excellent performance!" -ForegroundColor Green
} elseif ($stopwatch.ElapsedMilliseconds -lt 2000) {
Write-Host "✅ Good performance" -ForegroundColor Yellow
} else {
Write-Host "⚠️ Could be faster" -ForegroundColor Red
}
}
catch {
$stopwatch.Stop()
Write-Host "❌ Error after $($stopwatch.ElapsedMilliseconds)ms: $($_.Exception.Message)" -ForegroundColor Red
}
-9
View File
@@ -1,9 +0,0 @@
import dynamic from 'next/dynamic';
const components = {
// other components
resourceUsage: dynamic(() => import('./resourceUsage/component')),
yourwidget: dynamic(() => import("./yourwidget/component"))
};
export default components;
-9
View File
@@ -1,9 +0,0 @@
import resourceUsage from "./resourceUsage/widget";
import yourwidget from "./yourwidget/widget";
const widgets = {
// other widgets
resourceUsage: resourceUsage,
yourwidget: yourwidget
};
export default widgets;
-37
View File
@@ -1,37 +0,0 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data, error } = useWidgetAPI(widget, "info");
if (error) {
return <Container service={service} error={error} />;
}
if (!data) {
return (
<Container service={service}>
<Block label="Loading..." />
</Container>
);
}
return (
<Container service={service}>
<Block label="Machine Name" value={data.ComputerInfo.MachineName} />
<Block label="OS Version" value={data.ComputerInfo.OSVersion} />
<Block label="OS Architecture" value={data.ComputerInfo.OSArchitecture} />
<Block label="Processor Count" value={data.ComputerInfo.ProcessorCount} />
<Block label="CPU Usage" value={data.CPU} />
<Block label="RAM Usage" value={data.RAM} />
<Block label="GPU Usage" value={data.GPU.Usage} />
<Block label="GPU Temperature" value={data.GPU.Temperature} />
<Block label="GPU Fan Speed" value={data.GPU.FanSpeed} />
<Block label="Currently Running Game" value={data.CurrentlyRunningGame} />
</Container>
);
}
-13
View File
@@ -1,13 +0,0 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "http://192.168.50.201:5000/api/resource-usage",
proxyHandler: genericProxyHandler,
mappings: {
info: {
endpoint: ""
}
}
};
export default widget;
-33
View File
@@ -1,33 +0,0 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data, error } = useWidgetAPI(widget, "info");
if (error) {
return <Container service={service} error={error} />;
}
if (!data) {
return (
<Container service={service}>
<Block label="yourwidget.key1" />
<Block label="yourwidget.key2" />
<Block label="yourwidget.key3" />
</Container>
);
}
return (
<Container service={service}>
<Block label="yourwidget.key1" value={t("common.number", { value: data.key1 })} />
<Block label="yourwidget.key2" value={t("common.number", { value: data.key2 })} />
<Block label="yourwidget.key3" value={t("common.number", { value: data.key3 })} />
</Container>
);
}
-14
View File
@@ -1,14 +0,0 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/{endpoint}" ,
proxyHandler: genericProxyHandler ,
mappings: {
info: {
endpoint: "v1/info" ,
},
},
};
export default widget;
+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:24142
echo API Documentation: http://localhost:24142/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:24142
echo.
dotnet run --configuration Release
pause
+56
View File
@@ -0,0 +1,56 @@
# Performance test script for ResourceUsage API
Write-Host "Waiting for service to start..." -ForegroundColor Yellow
Start-Sleep -Seconds 5
Write-Host "Testing API response time..." -ForegroundColor Green
$times = @()
$errors = 0
for ($i = 1; $i -le 5; $i++) {
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
try {
$response = Invoke-WebRequest -Uri "http://localhost:5000/api/resource/usage" -UseBasicParsing -TimeoutSec 30
$stopwatch.Stop()
$time = $stopwatch.ElapsedMilliseconds
$times += $time
Write-Host "Test $i - Response time: ${time}ms - Status: $($response.StatusCode)" -ForegroundColor Cyan
# Show first response content for verification
if ($i -eq 1) {
$jsonContent = $response.Content | ConvertFrom-Json
Write-Host "Sample response - CPU Usage: $($jsonContent.CPU.Usage)%" -ForegroundColor White
}
}
catch {
$stopwatch.Stop()
$errors++
Write-Host "Test $i - Error after $($stopwatch.ElapsedMilliseconds)ms: $($_.Exception.Message)" -ForegroundColor Red
}
if ($i -lt 5) {
Start-Sleep -Seconds 2
}
}
if ($times.Count -gt 0) {
$avgTime = ($times | Measure-Object -Average).Average
$minTime = ($times | Measure-Object -Minimum).Minimum
$maxTime = ($times | Measure-Object -Maximum).Maximum
Write-Host "`nPerformance Results:" -ForegroundColor Green
Write-Host " Average response time: $([math]::Round($avgTime, 2))ms" -ForegroundColor White
Write-Host " Minimum response time: ${minTime}ms" -ForegroundColor White
Write-Host " Maximum response time: ${maxTime}ms" -ForegroundColor White
Write-Host " Successful requests: $($times.Count)/5" -ForegroundColor White
Write-Host " Failed requests: $errors/5" -ForegroundColor White
if ($avgTime -lt 1000) {
Write-Host "✅ Performance looks good!" -ForegroundColor Green
} elseif ($avgTime -lt 3000) {
Write-Host "⚠️ Performance is acceptable but could be better" -ForegroundColor Yellow
} else {
Write-Host "❌ Performance needs improvement" -ForegroundColor Red
}
} else {
Write-Host "❌ All requests failed!" -ForegroundColor Red
}
+136
View File
@@ -0,0 +1,136 @@
/* Custom styles for Resource Monitor Dashboard */
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.slide-down {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* System Control Button Animations */
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
}
}
/* Make title elements more touch-friendly on mobile */
@media (max-width: 768px) {
nav h1 {
padding: 8px;
border-radius: 4px;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
}
nav h1:active {
background-color: rgba(255, 255, 255, 0.1);
}
nav .fas.fa-chart-line {
padding: 8px;
border-radius: 50%;
cursor: pointer;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
}
nav .fas.fa-chart-line:active {
background-color: rgba(255, 255, 255, 0.1);
}
}
/* Custom progress bar animations */
.progress-bar {
transition: width 0.5s ease-in-out;
}
/* Responsive table */
@media (max-width: 768px) {
.mobile-table {
font-size: 0.875rem;
}
.mobile-table th,
.mobile-table td {
padding: 0.5rem 0.25rem;
}
}
/* Custom notification styles */
.notification {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
max-width: 400px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Process table hover effects */
.process-row:hover {
background-color: #f9fafb;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
/* Chart container responsive */
.chart-container {
position: relative;
height: 200px;
width: 100%;
}
@media (max-width: 640px) {
.chart-container {
height: 150px;
}
}
+3
View File
@@ -0,0 +1,3 @@
<!-- This is a placeholder for a favicon. In a real deployment, you would place an actual favicon.ico file here -->
<!-- For now, we'll use a Font Awesome icon as a favicon alternative -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📊</text></svg>">
+258
View File
@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Resource Monitor Dashboard</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="css/dashboard.css">
</head>
<body class="bg-gray-100">
<!-- Navigation -->
<nav class="bg-blue-600 text-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<i class="fas fa-chart-line text-2xl mr-3"></i>
<h1 class="text-xl font-bold">Resource Monitor</h1>
</div>
<div class="flex space-x-4">
<button id="toggleAutoRefresh" class="bg-yellow-500 hover:bg-yellow-700 px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-sync mr-2"></i>Auto: ON
</button>
<button id="toggleProcesses" class="bg-purple-500 hover:bg-purple-700 px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-list mr-2"></i>Processes
</button>
<button id="toggleDetails" class="bg-blue-500 hover:bg-blue-700 px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-info-circle mr-2"></i>Details
</button>
<button id="refreshData" class="bg-green-500 hover:bg-green-700 px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-sync-alt mr-2"></i>Refresh
</button>
<!-- Hidden System Control Button (requires triple-click to show) -->
<button id="systemControl" class="hidden bg-red-600 hover:bg-red-800 px-4 py-2 rounded-lg transition-colors" title="System Control">
<i class="fas fa-power-off mr-2"></i>System
</button>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Dashboard Overview -->
<div id="dashboard" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- CPU Card -->
<div class="bg-white rounded-lg shadow-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">CPU Usage</p>
<p id="cpuUsage" class="text-3xl font-bold text-blue-600">0%</p>
</div>
<div class="bg-blue-100 p-3 rounded-full">
<i class="fas fa-microchip text-blue-600 text-xl"></i>
</div>
</div>
<div class="mt-4">
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="cpuBar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Memory Card -->
<div class="bg-white rounded-lg shadow-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Memory Usage</p>
<p id="memoryUsage" class="text-3xl font-bold text-green-600">0%</p>
</div>
<div class="bg-green-100 p-3 rounded-full">
<i class="fas fa-memory text-green-600 text-xl"></i>
</div>
</div>
<div class="mt-4">
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="memoryBar" class="bg-green-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<!-- GPU Card -->
<div class="bg-white rounded-lg shadow-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">GPU Usage</p>
<p id="gpuUsage" class="text-3xl font-bold text-purple-600">0%</p>
</div>
<div class="bg-purple-100 p-3 rounded-full">
<i class="fas fa-display text-purple-600 text-xl"></i>
</div>
</div>
<div class="mt-4">
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="gpuBar" class="bg-purple-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="mt-2 flex items-center justify-between text-sm">
<span class="text-gray-500">
<i class="fas fa-thermometer-half mr-1"></i>
<span id="gpuTemp" class="font-medium">0°C</span>
</span>
<span id="gpuTempStatus" class="text-green-600">Normal</span>
</div>
</div>
</div>
</div>
<!-- Game Detection Section -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-gamepad mr-2"></i>Game Detection
</h2>
<span id="gameStatus" class="text-sm text-gray-500">No game detected</span>
</div>
<div id="gameInfo" class="text-center py-8">
<div class="text-gray-400 mb-4">
<i class="fas fa-gamepad text-4xl"></i>
</div>
<p class="text-gray-500">No game currently running</p>
</div>
</div>
<!-- Top Processes Section (Hidden by default) -->
<div id="processesSection" class="hidden">
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-list mr-2"></i>Top Processes
</h2>
<span class="text-sm text-gray-500">Click process name to terminate</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-full table-auto">
<thead>
<tr class="bg-gray-50">
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Process</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CPU %</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory %</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody id="processTable" class="bg-white divide-y divide-gray-200">
<!-- Process rows will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Detailed Information (Hidden by default) -->
<div id="detailsSection" class="hidden">
<!-- System Information -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
<h2 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-server mr-2"></i>System Information
</h2>
<div id="systemInfo" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- System info will be populated here -->
</div>
</div>
<!-- Disk Usage -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
<h2 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-hard-drive mr-2"></i>Disk Usage
</h2>
<div id="diskUsage" class="space-y-4">
<!-- Disk usage will be populated here -->
</div>
</div>
<!-- Performance Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow-lg p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4">CPU History</h3>
<canvas id="cpuChart" width="400" height="200"></canvas>
</div>
<div class="bg-white rounded-lg shadow-lg p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4">Memory History</h3>
<canvas id="memoryChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="text-gray-700">Loading...</span>
</div>
</div>
<!-- System Control Modal -->
<div id="systemControlModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-gray-800">
<i class="fas fa-power-off mr-2 text-red-600"></i>System Control
</h3>
<button id="closeSystemModal" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="space-y-4">
<!-- Timer Input -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Timer (seconds) - Default: 15 seconds
</label>
<input type="number" id="systemTimer"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="15" min="0" max="86400" value="15">
<p class="text-xs text-gray-500 mt-1">Maximum: 24 hours (86400 seconds)</p>
</div>
<!-- Action Buttons -->
<div class="grid grid-cols-3 gap-3">
<button id="shutdownBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
<i class="fas fa-power-off mr-2"></i>Shutdown
</button>
<button id="restartBtn" class="bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
<i class="fas fa-redo mr-2"></i>Restart
</button>
<button id="cancelBtn" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
<i class="fas fa-ban mr-2"></i>Cancel
</button>
</div>
<!-- Force Shutdown Option -->
<div class="flex items-center">
<input type="checkbox" id="forceShutdown" class="mr-2" checked>
<label for="forceShutdown" class="text-sm text-gray-700">Force shutdown (close applications without saving)</label>
</div>
<!-- Warning -->
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div class="flex">
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2 mt-0.5"></i>
<div class="text-sm text-yellow-800">
<strong>Warning:</strong> This will shut down or restart the entire system.
Make sure to save any unsaved work first. Use the Cancel button to abort any pending shutdown/restart.
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/dashboard.js"></script>
</body>
</html>
+972
View File
@@ -0,0 +1,972 @@
// Dashboard JavaScript
class ResourceDashboard {
constructor() {
this.connection = null;
this.cpuChart = null;
this.memoryChart = null;
this.cpuHistory = [];
this.memoryHistory = [];
this.maxHistoryPoints = 20;
this.lastResourceData = null; // Store latest resource data
this.lastSystemInfo = null; // Store latest system info
this.autoRefreshEnabled = true; // Auto-refresh toggle
this.refreshInterval = null; // Manual refresh interval
this.init();
}
async init() {
this.setupEventListeners();
this.initializeCharts();
await this.connectSignalR();
await this.loadInitialData();
this.hideLoading();
this.startAutoRefresh(); // Use our new auto-refresh system
}
setupEventListeners() {
document.getElementById('toggleAutoRefresh').addEventListener('click', () => {
this.toggleAutoRefresh();
});
document.getElementById('toggleProcesses').addEventListener('click', () => {
this.toggleProcessesSection();
});
document.getElementById('toggleDetails').addEventListener('click', () => {
this.toggleDetailsSection();
});
document.getElementById('refreshData').addEventListener('click', () => {
this.refreshData();
});
// System control event listeners
document.getElementById('systemControl').addEventListener('click', () => {
this.showSystemControlModal();
});
document.getElementById('closeSystemModal').addEventListener('click', () => {
this.hideSystemControlModal();
});
document.getElementById('shutdownBtn').addEventListener('click', () => {
this.executeSystemCommand('shutdown');
});
document.getElementById('restartBtn').addEventListener('click', () => {
this.executeSystemCommand('restart');
});
document.getElementById('cancelBtn').addEventListener('click', () => {
this.cancelSystemCommand();
});
// Hidden system control activation methods
this.setupHiddenSystemControlAccess();
// Close modal when clicking outside
document.getElementById('systemControlModal').addEventListener('click', (e) => {
if (e.target.id === 'systemControlModal') {
this.hideSystemControlModal();
}
});
}
setupHiddenSystemControlAccess() {
// Method 1: Triple click on title (works on mobile)
let clickCount = 0;
let clickTimer = null;
const titleElement = document.querySelector('h1');
titleElement.addEventListener('click', () => {
clickCount++;
if (clickCount === 1) {
clickTimer = setTimeout(() => {
clickCount = 0;
}, 2000); // Reset after 2 seconds
} else if (clickCount === 3) {
clearTimeout(clickTimer);
clickCount = 0;
this.toggleSystemControlButton();
}
});
// Method 2: Long press on title (mobile-friendly)
let pressTimer = null;
let pressStartTime = 0;
titleElement.addEventListener('touchstart', (e) => {
pressStartTime = Date.now();
pressTimer = setTimeout(() => {
// Vibrate if supported (mobile feedback)
if (navigator.vibrate) {
navigator.vibrate(200);
}
this.toggleSystemControlButton();
}, 2000); // 2 second long press
});
titleElement.addEventListener('touchend', () => {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
});
titleElement.addEventListener('touchmove', () => {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
});
// Method 3: Mouse long press (desktop fallback)
titleElement.addEventListener('mousedown', (e) => {
if (e.button === 0) { // Left mouse button
pressStartTime = Date.now();
pressTimer = setTimeout(() => {
this.toggleSystemControlButton();
}, 2000);
}
});
titleElement.addEventListener('mouseup', () => {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
});
titleElement.addEventListener('mouseleave', () => {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
});
// Method 4: Secret tap sequence on the favicon
let tapSequence = [];
const favicon = document.querySelector('nav .fas.fa-chart-line');
favicon.addEventListener('click', () => {
tapSequence.push(Date.now());
// Keep only last 5 taps within 3 seconds
const now = Date.now();
tapSequence = tapSequence.filter(time => now - time < 3000);
// Check for specific pattern: 5 quick taps
if (tapSequence.length >= 5) {
const timeDiffs = [];
for (let i = 1; i < tapSequence.length; i++) {
timeDiffs.push(tapSequence[i] - tapSequence[i-1]);
}
// All taps should be within 200ms of each other
const isQuickSequence = timeDiffs.every(diff => diff < 500);
if (isQuickSequence) {
if (navigator.vibrate) {
navigator.vibrate([100, 50, 100]);
}
this.toggleSystemControlButton();
tapSequence = [];
}
}
});
// Method 5: Keyboard shortcut (desktop)
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
e.preventDefault();
this.toggleSystemControlButton();
}
});
}
toggleDetailsSection() {
const detailsSection = document.getElementById('detailsSection');
const toggleButton = document.getElementById('toggleDetails');
if (detailsSection.classList.contains('hidden')) {
detailsSection.classList.remove('hidden');
toggleButton.innerHTML = '<i class="fas fa-info-circle mr-2"></i>Hide Details';
// Refresh disk usage when details section becomes visible
this.refreshDetailsData();
} else {
detailsSection.classList.add('hidden');
toggleButton.innerHTML = '<i class="fas fa-info-circle mr-2"></i>Details';
}
}
toggleProcessesSection() {
const processesSection = document.getElementById('processesSection');
const toggleButton = document.getElementById('toggleProcesses');
if (processesSection.classList.contains('hidden')) {
processesSection.classList.remove('hidden');
toggleButton.innerHTML = '<i class="fas fa-list mr-2"></i>Hide Processes';
// Refresh processes when section becomes visible
this.refreshProcessesData();
} else {
processesSection.classList.add('hidden');
toggleButton.innerHTML = '<i class="fas fa-list mr-2"></i>Processes';
}
}
async refreshProcessesData() {
try {
// If we have cached data, use it immediately
if (this.lastResourceData && this.lastResourceData.topProcesses) {
this.updateProcessTable(this.lastResourceData.topProcesses);
}
// Then fetch fresh data
const resourceUsage = await this.fetchData('/api/resource/usage');
this.updateProcessTable(resourceUsage.topProcesses);
} catch (error) {
console.error('Error refreshing processes data:', error);
}
}
async refreshDetailsData() {
try {
// If we have cached data, use it immediately
if (this.lastResourceData && this.lastResourceData.disks) {
this.updateDiskUsage(this.lastResourceData.disks);
}
if (this.lastSystemInfo) {
this.updateSystemInfo(this.lastSystemInfo);
}
// Then fetch fresh data
const [resourceUsage, systemInfo] = await Promise.all([
this.fetchData('/api/resource/usage'),
this.fetchData('/api/resource/system-info')
]);
this.updateDiskUsage(resourceUsage.disks);
this.updateSystemInfo(systemInfo);
} catch (error) {
console.error('Error refreshing details data:', error);
}
}
toggleAutoRefresh() {
this.autoRefreshEnabled = !this.autoRefreshEnabled;
const toggleButton = document.getElementById('toggleAutoRefresh');
if (this.autoRefreshEnabled) {
toggleButton.innerHTML = '<i class="fas fa-sync mr-2"></i>Auto: ON';
toggleButton.className = 'bg-yellow-500 hover:bg-yellow-700 px-4 py-2 rounded-lg transition-colors';
this.startAutoRefresh();
} else {
toggleButton.innerHTML = '<i class="fas fa-pause mr-2"></i>Auto: OFF';
toggleButton.className = 'bg-gray-500 hover:bg-gray-700 px-4 py-2 rounded-lg transition-colors';
this.stopAutoRefresh();
}
console.log(`Auto-refresh ${this.autoRefreshEnabled ? 'enabled' : 'disabled'}`);
}
startAutoRefresh() {
// Connect SignalR for real-time updates
if (!this.connection || this.connection.state === 'Disconnected') {
this.connectSignalR();
}
// Also start a fallback manual refresh timer (every 60 seconds as backup)
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.refreshInterval = setInterval(() => {
if (this.autoRefreshEnabled) {
this.refreshData();
}
}, 300000); // 300 second fallback
}
stopAutoRefresh() {
// Disconnect SignalR
if (this.connection && this.connection.state === 'Connected') {
this.connection.stop();
}
// Clear manual refresh timer
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
async connectSignalR() {
try {
// Only connect if auto-refresh is enabled
if (!this.autoRefreshEnabled) {
console.log("SignalR connection skipped - auto-refresh disabled");
return;
}
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/resourceHub")
.build();
this.connection.on("ResourceUpdate", (data) => {
if (this.autoRefreshEnabled) {
this.updateDashboard(data);
}
});
await this.connection.start();
console.log("SignalR Connected");
} catch (err) {
console.error("SignalR Connection Error: ", err);
}
}
async loadInitialData() {
try {
const [resourceUsage, systemInfo] = await Promise.all([
this.fetchData('/api/resource/usage'),
this.fetchData('/api/resource/system-info')
]);
this.updateDashboard(resourceUsage);
this.updateSystemInfo(systemInfo);
} catch (error) {
console.error('Error loading initial data:', error);
}
}
async fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
updateDashboard(data) {
try {
// Store the latest data for when details section is opened
this.lastResourceData = data;
// Update CPU
if (data.cpu) {
const cpuUsage = data.cpu.usage || 0;
document.getElementById('cpuUsage').textContent = `${cpuUsage.toFixed(1)}%`;
document.getElementById('cpuBar').style.width = `${cpuUsage}%`;
this.updateCpuChart(cpuUsage);
}
// Update Memory
if (data.memory) {
const memUsage = data.memory.usagePercentage || 0;
document.getElementById('memoryUsage').textContent = `${memUsage.toFixed(1)}%`;
document.getElementById('memoryBar').style.width = `${memUsage}%`;
this.updateMemoryChart(memUsage);
}
// Update GPU
if (data.gpu) {
const gpuUsage = data.gpu.usage || 0;
const gpuTemp = data.gpu.temperature || 0;
document.getElementById('gpuUsage').textContent = `${gpuUsage.toFixed(1)}%`;
document.getElementById('gpuBar').style.width = `${gpuUsage}%`;
// Update GPU temperature
document.getElementById('gpuTemp').textContent = `${gpuTemp}°C`;
// Set temperature status color based on temperature ranges
const tempStatusElement = document.getElementById('gpuTempStatus');
if (gpuTemp <= 60) {
tempStatusElement.textContent = 'Cool';
tempStatusElement.className = 'text-green-600';
} else if (gpuTemp <= 75) {
tempStatusElement.textContent = 'Normal';
tempStatusElement.className = 'text-yellow-600';
} else if (gpuTemp <= 85) {
tempStatusElement.textContent = 'Warm';
tempStatusElement.className = 'text-orange-600';
} else {
tempStatusElement.textContent = 'Hot';
tempStatusElement.className = 'text-red-600';
}
}
// Update Game Detection
if (data.runningGame) {
this.updateGameInfo(data.runningGame);
} else {
this.updateGameInfo(null);
}
// Update Processes (only if processes section is visible)
if (data.topProcesses && Array.isArray(data.topProcesses) && !document.getElementById('processesSection').classList.contains('hidden')) {
this.updateProcessTable(data.topProcesses);
}
// Update Disk Usage (only if details section is visible)
if (data.disks && !document.getElementById('detailsSection').classList.contains('hidden')) {
this.updateDiskUsage(data.disks);
}
// Update System Info GPU details (only if details section is visible and we have system info)
if (!document.getElementById('detailsSection').classList.contains('hidden') && this.lastSystemInfo) {
this.updateSystemInfo(this.lastSystemInfo);
}
} catch (error) {
console.error('Error updating dashboard:', error);
this.showNotification('Error updating dashboard data', 'error');
}
}
updateProcessTable(processes) {
const tableBody = document.getElementById('processTable');
tableBody.innerHTML = '';
if (!processes || !Array.isArray(processes)) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="5" class="px-4 py-4 text-center text-gray-500">No process data available</td>';
tableBody.appendChild(row);
return;
}
processes.slice(0, 10).forEach((process, index) => {
if (!process) return;
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
const killButtonClass = index < 3 ?
'bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded text-xs transition-colors' :
'bg-gray-300 text-gray-500 px-2 py-1 rounded text-xs cursor-not-allowed';
const killButtonDisabled = index >= 3 ? 'disabled' : '';
const processName = process.name || 'Unknown';
const processId = process.processId || 0;
const cpuUsage = process.cpuUsage || 0;
const memoryUsage = process.memoryUsage || 0;
const memoryUsagePercentage = process.memoryUsagePercentage || 0;
row.innerHTML = `
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">${processName}</div>
<div class="text-sm text-gray-500 ml-2">PID: ${processId}</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
${cpuUsage.toFixed(1)}%
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
${(memoryUsage / 1024 / 1024).toFixed(1)} MB
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
${memoryUsagePercentage.toFixed(1)}%
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
<button onclick="dashboard.killProcess(${processId}, '${processName}')"
class="${killButtonClass}" ${killButtonDisabled}>
<i class="fas fa-times mr-1"></i>Kill
</button>
</td>
`;
tableBody.appendChild(row);
});
}
async killProcess(processId, processName) {
if (!confirm(`Are you sure you want to terminate "${processName}" (PID: ${processId})?`)) {
return;
}
try {
const response = await fetch(`/api/resource/kill-process/${processId}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
this.showNotification(result.message, 'success');
await this.refreshData();
} else {
const error = await response.text();
this.showNotification(`Failed to kill process: ${error}`, 'error');
}
} catch (error) {
console.error('Error killing process:', error);
this.showNotification('Error killing process', 'error');
}
}
updateSystemInfo(systemInfo) {
// Store the latest system info
this.lastSystemInfo = systemInfo;
// Get GPU info from the latest resource data if available
const gpuInfo = this.lastResourceData?.gpu;
let gpuSection = '';
if (gpuInfo && gpuInfo.isAvailable) {
gpuSection = `
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">GPU</h4>
<p class="text-gray-600">${gpuInfo.name || 'Unknown GPU'}</p>
<div class="mt-2 space-y-1">
<div class="flex justify-between text-sm">
<span class="text-gray-500">Usage:</span>
<span class="text-gray-700">${gpuInfo.usage || 0}%</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">Temperature:</span>
<span class="text-gray-700 ${gpuInfo.temperature > 85 ? 'text-red-600' : gpuInfo.temperature > 75 ? 'text-orange-600' : gpuInfo.temperature > 60 ? 'text-yellow-600' : 'text-green-600'}">${gpuInfo.temperature || 0}°C</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">Memory:</span>
<span class="text-gray-700">${gpuInfo.memoryUsed && gpuInfo.memoryTotal ? ((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100).toFixed(1) : 0}%</span>
</div>
${gpuInfo.fanSpeed ? `
<div class="flex justify-between text-sm">
<span class="text-gray-500">Fan Speed:</span>
<span class="text-gray-700">${gpuInfo.fanSpeed}%</span>
</div>` : ''}
${gpuInfo.powerUsage ? `
<div class="flex justify-between text-sm">
<span class="text-gray-500">Power:</span>
<span class="text-gray-700">${gpuInfo.powerUsage}W</span>
</div>` : ''}
</div>
</div>
`;
}
const systemInfoDiv = document.getElementById('systemInfo');
systemInfoDiv.innerHTML = `
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Machine Name</h4>
<p class="text-gray-600">${systemInfo.machineName || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">OS Version</h4>
<p class="text-gray-600">${systemInfo.osVersion || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">CPU</h4>
<p class="text-gray-600">${systemInfo.cpuName || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Processor Count</h4>
<p class="text-gray-600">${systemInfo.processorCount || 0} cores</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Total Memory</h4>
<p class="text-gray-600">${systemInfo.totalPhysicalMemory ? (systemInfo.totalPhysicalMemory / 1024 / 1024 / 1024).toFixed(1) : 'N/A'} GB</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Uptime</h4>
<p class="text-gray-600">${systemInfo.uptime ? this.formatUptime(systemInfo.uptime) : 'N/A'}</p>
</div>
${gpuSection}
`;
}
updateDiskUsage(disks) {
const diskUsageDiv = document.getElementById('diskUsage');
diskUsageDiv.innerHTML = '';
if (!disks || !Array.isArray(disks)) {
diskUsageDiv.innerHTML = '<p class="text-gray-500">No disk information available</p>';
return;
}
disks.forEach(disk => {
if (!disk || !disk.totalSize || !disk.freeSpace) return;
const usagePercentage = ((disk.totalSize - disk.freeSpace) / disk.totalSize * 100);
const diskName = disk.label ? `${disk.driveLetter} (${disk.label})` : disk.driveLetter || 'Unknown Drive';
const diskDiv = document.createElement('div');
diskDiv.className = 'bg-gray-50 p-4 rounded-lg';
diskDiv.innerHTML = `
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold text-gray-700">${diskName}</h4>
<span class="text-sm text-gray-500">${usagePercentage.toFixed(1)}% used</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: ${usagePercentage}%"></div>
</div>
<div class="flex justify-between text-sm text-gray-600">
<span>Free: ${(disk.freeSpace / 1024 / 1024 / 1024).toFixed(1)} GB</span>
<span>Total: ${(disk.totalSize / 1024 / 1024 / 1024).toFixed(1)} GB</span>
</div>
`;
diskUsageDiv.appendChild(diskDiv);
});
}
updateGameInfo(gameInfo) {
console.log('updateGameInfo called with:', gameInfo); // Debug log
const gameStatusSpan = document.getElementById('gameStatus');
const gameInfoDiv = document.getElementById('gameInfo');
if (!gameInfo) {
console.log('No game detected, showing default state'); // Debug log
gameStatusSpan.textContent = 'No game detected';
gameInfoDiv.innerHTML = `
<div class="text-center py-8">
<div class="text-gray-400 mb-4">
<i class="fas fa-gamepad text-4xl"></i>
</div>
<p class="text-gray-500">No game currently running</p>
</div>
`;
return;
}
console.log('Game detected:', gameInfo.gameName); // Debug log
gameStatusSpan.textContent = 'Game detected';
gameStatusSpan.className = 'text-sm text-green-600 font-semibold';
gameInfoDiv.innerHTML = `
<div class="bg-gradient-to-r from-purple-50 to-blue-50 p-6 rounded-lg">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="bg-green-100 p-3 rounded-full mr-4">
<i class="fas fa-gamepad text-green-600 text-2xl"></i>
</div>
<div>
<h3 class="text-xl font-bold text-gray-800">${gameInfo.gameName || 'Unknown Game'}</h3>
<p class="text-gray-600">Running since ${this.formatGameStartTime(gameInfo.startTime)}</p>
</div>
</div>
<div class="flex items-center space-x-3">
<div class="bg-green-500 text-white px-3 py-1 rounded-full text-sm font-medium">
<i class="fas fa-circle text-xs mr-1"></i>ACTIVE
</div>
<button onclick="dashboard.killProcess(${gameInfo.processId}, '${gameInfo.gameName || 'Unknown Game'}')"
class="bg-red-500 hover:bg-red-700 text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors">
<i class="fas fa-times mr-1"></i>End Game
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">Process ID</h4>
<p class="text-gray-600">${gameInfo.processId || 'N/A'}</p>
</div>
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">Memory Usage</h4>
<p class="text-gray-600">${gameInfo.memoryUsage ? (gameInfo.memoryUsage / 1024 / 1024).toFixed(1) + ' MB' : 'N/A'}</p>
</div>
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">CPU Usage</h4>
<p class="text-gray-600">${gameInfo.cpuUsage ? gameInfo.cpuUsage.toFixed(1) + '%' : 'N/A'}</p>
</div>
</div>
</div>
`;
}
formatGameStartTime(startTime) {
if (!startTime) return 'Unknown';
try {
const date = new Date(startTime);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
if (diffHours > 0) {
return `${diffHours}h ${diffMins % 60}m ago`;
} else {
return `${diffMins}m ago`;
}
} catch (error) {
return 'Unknown';
}
}
initializeCharts() {
// CPU Chart
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
this.cpuChart = new Chart(cpuCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU Usage %',
data: [],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
// Memory Chart
const memoryCtx = document.getElementById('memoryChart').getContext('2d');
this.memoryChart = new Chart(memoryCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Memory Usage %',
data: [],
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
updateCpuChart(cpuUsage) {
this.cpuHistory.push(cpuUsage);
if (this.cpuHistory.length > this.maxHistoryPoints) {
this.cpuHistory.shift();
}
this.cpuChart.data.labels = this.cpuHistory.map((_, index) => '');
this.cpuChart.data.datasets[0].data = this.cpuHistory;
this.cpuChart.update('none');
}
updateMemoryChart(memoryUsage) {
this.memoryHistory.push(memoryUsage);
if (this.memoryHistory.length > this.maxHistoryPoints) {
this.memoryHistory.shift();
}
this.memoryChart.data.labels = this.memoryHistory.map((_, index) => '');
this.memoryChart.data.datasets[0].data = this.memoryHistory;
this.memoryChart.update('none');
}
async refreshData() {
try {
console.log('Manual refresh triggered');
const resourceUsage = await this.fetchData('/api/resource/usage');
this.updateDashboard(resourceUsage);
// Show brief success feedback
this.showNotification('Data refreshed successfully', 'success');
} catch (error) {
console.error('Error refreshing data:', error);
this.showNotification('Error refreshing data', 'error');
}
}
// Legacy method - now handled by startAutoRefresh()
startDataRefresh() {
console.log("Using new auto-refresh system instead");
this.startAutoRefresh();
}
formatUptime(uptime) {
// Parse TimeSpan string format "d.hh:mm:ss.fffffff"
if (typeof uptime === 'string') {
const parts = uptime.split('.');
const days = parseInt(parts[0]) || 0;
if (parts[1]) {
const timeParts = parts[1].split(':');
const hours = parseInt(timeParts[0]) || 0;
const minutes = parseInt(timeParts[1]) || 0;
return `${days}d ${hours}h ${minutes}m`;
}
return `${days}d 0h 0m`;
}
// Fallback for object format
const days = Math.floor(uptime.totalDays || 0);
const hours = Math.floor(uptime.hours || 0);
const minutes = Math.floor(uptime.minutes || 0);
return `${days}d ${hours}h ${minutes}m`;
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
} text-white`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// System Control Methods
toggleSystemControlButton() {
const systemButton = document.getElementById('systemControl');
if (systemButton.classList.contains('hidden')) {
systemButton.classList.remove('hidden');
// Add visual feedback with pulsing effect
systemButton.style.animation = 'pulse 1s ease-in-out 3';
// Show mobile-friendly notification
this.showNotification('🔓 System control unlocked! Tap the red System button to access shutdown/restart options.', 'info');
// Auto-hide after 30 seconds for security
setTimeout(() => {
if (!systemButton.classList.contains('hidden')) {
systemButton.classList.add('hidden');
this.hideSystemControlModal();
this.showNotification('System control auto-locked for security', 'info');
}
}, 30000);
} else {
systemButton.classList.add('hidden');
systemButton.style.animation = '';
this.hideSystemControlModal();
this.showNotification('System control locked', 'info');
}
}
showSystemControlModal() {
document.getElementById('systemControlModal').classList.remove('hidden');
document.getElementById('systemTimer').value = '15'; // Default 15 seconds
document.getElementById('forceShutdown').checked = true;
}
hideSystemControlModal() {
document.getElementById('systemControlModal').classList.add('hidden');
}
async executeSystemCommand(action) {
const timer = document.getElementById('systemTimer').value;
const force = document.getElementById('forceShutdown').checked;
// Validate timer input - default to 15 if empty
const timerSeconds = parseInt(timer) || 15;
if (timerSeconds < 0 || timerSeconds > 86400) {
this.showNotification('Timer must be between 0 and 86400 seconds (24 hours)', 'error');
return;
}
// Build confirmation message
let confirmMessage = `Are you sure you want to ${action} the system`;
if (timerSeconds > 0) {
const minutes = Math.floor(timerSeconds / 60);
const seconds = timerSeconds % 60;
if (minutes > 0) {
confirmMessage += ` in ${minutes} minute${minutes !== 1 ? 's' : ''}`;
if (seconds > 0) {
confirmMessage += ` and ${seconds} second${seconds !== 1 ? 's' : ''}`;
}
} else {
confirmMessage += ` in ${seconds} second${seconds !== 1 ? 's' : ''}`;
}
} else {
confirmMessage += ' immediately';
}
confirmMessage += '?\n\nNote: You can cancel this action using the Cancel button.';
if (!confirm(confirmMessage)) {
return;
}
try {
// Prepare the command data
const commandData = {
action: action,
timer: timerSeconds,
force: force
};
const response = await fetch('/api/resource/system-control', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(commandData)
});
if (response.ok) {
const result = await response.json();
this.showNotification(result.message, 'success');
this.hideSystemControlModal();
// If immediate action, warn user
if (timerSeconds === 0) {
setTimeout(() => {
this.showNotification(`System ${action} initiated! Connection will be lost.`, 'info');
}, 1000);
} else {
// Show reminder about cancel option
setTimeout(() => {
this.showNotification(`Reminder: Use the Cancel button to abort the ${action} if needed.`, 'info');
}, 2000);
}
} else {
const error = await response.text();
this.showNotification(`Failed to ${action} system: ${error}`, 'error');
}
} catch (error) {
console.error(`Error executing ${action}:`, error);
this.showNotification(`Error executing ${action} command`, 'error');
}
}
async cancelSystemCommand() {
if (!confirm('Are you sure you want to cancel any pending shutdown/restart?')) {
return;
}
try {
const response = await fetch('/api/resource/cancel-shutdown', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
this.showNotification(result.message, 'success');
} else {
const error = await response.text();
this.showNotification(`Failed to cancel shutdown: ${error}`, 'error');
}
} catch (error) {
console.error('Error canceling shutdown:', error);
this.showNotification('Error canceling shutdown command', 'error');
}
}
hideLoading() {
document.getElementById('loadingOverlay').style.display = 'none';
}
}
// Initialize dashboard when page loads
let dashboard;
document.addEventListener('DOMContentLoaded', () => {
dashboard = new ResourceDashboard();
});
+78
View File
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Table Test</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-bold mb-6">Process Table with Memory Percentage</h1>
<div class="bg-white rounded-lg shadow-lg p-6">
<h2 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-list mr-2"></i>Top Processes
</h2>
<div class="overflow-x-auto">
<table class="min-w-full table-auto">
<thead>
<tr class="bg-gray-50">
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Process</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CPU %</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory %</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-gray-50">
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">chrome.exe</div>
<div class="text-sm text-gray-500 ml-2">PID: 1234</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">15.2%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">512.3 MB</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">3.2%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
<button class="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded text-xs">
Kill
</button>
</td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">notepad.exe</div>
<div class="text-sm text-gray-500 ml-2">PID: 5678</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">2.1%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">25.6 MB</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">0.2%</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
<button class="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded text-xs">
Kill
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-6 p-4 bg-green-100 border border-green-200 rounded-lg">
<h3 class="font-bold text-green-800">✅ Changes Made:</h3>
<ul class="list-disc list-inside text-green-700 mt-2">
<li>Added "Memory %" column to the process table</li>
<li>Updated HTML table header to include the new column</li>
<li>Modified JavaScript to display memory percentage from backend data</li>
<li>Backend already calculates MemoryUsagePercentage based on total system memory</li>
<li>Updated colspan for "No data" message from 4 to 5 columns</li>
</ul>
</div>
</div>
</body>
</html>