Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d1c55468b | |||
| bb7c4c3d0e | |||
| eceec1b72d | |||
| 1129f9a2b1 | |||
| 5ece1fbe27 | |||
| 35828f189c | |||
| e842b7d73e | |||
| 96b6e3dcd9 | |||
| 3d47fc1439 | |||
| aa30c9f034 | |||
| f2a0818d0e | |||
| d6efa9163b | |||
| 774cdbaf66 | |||
| 3f64ace8a7 | |||
| 3b3bdf3d46 | |||
| 823e467078 | |||
| ea2fe47263 | |||
| 294438145a | |||
| 413360ece2 | |||
| a0b9f05ae3 | |||
| caa7436d51 | |||
| 5eec358b68 | |||
| 7c1cbb44f8 | |||
| 06ea991a6c |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,594 @@
|
|||||||
# dotnet
|
# Resource Monitor Service
|
||||||
|
|
||||||
dotnet run
|
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.
|
||||||
git add .
|
|
||||||
git commit -m "Add steam running games"
|
## 🌟 New: Web Dashboard
|
||||||
git push origin master
|
|
||||||
dotnet publish -c Release -o ./publish
|
Access the interactive web dashboard:
|
||||||
|
- **Development**: `http://localhost:5000`
|
||||||
|
- **Release/Production**: `http://localhost:24142`
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
```
|
||||||
|
Then open your browser to:
|
||||||
|
- Development: `http://localhost:5000`
|
||||||
|
- Release/Production: `http://localhost:24142`
|
||||||
|
|
||||||
|
for the interactive dashboard.
|
||||||
|
|
||||||
|
### Option 2: Windows Service (Production)
|
||||||
|
```powershell
|
||||||
|
# Run as Administrator
|
||||||
|
cd C:\Work\DEV\ResourceUsageAPI
|
||||||
|
.\install-service.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Linux Service (if running on Linux)
|
||||||
|
```bash
|
||||||
|
cd /path/to/ResourceUsageAPI
|
||||||
|
chmod +x install-service.sh
|
||||||
|
sudo ./install-service.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Web Dashboard Features
|
||||||
|
|
||||||
|
### Dashboard Overview
|
||||||
|
- **Resource Cards**: CPU, Memory, GPU, and Network usage with visual progress bars
|
||||||
|
- **Game Detection**: Prominent section showing currently running games
|
||||||
|
- **Auto-refresh Toggle**: Control automatic updates (15-second intervals)
|
||||||
|
- **Manual Refresh**: Force immediate data updates
|
||||||
|
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## 📊 Service Management
|
||||||
|
|
||||||
|
### Starting and Stopping the Service
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Start the service
|
||||||
|
Start-Service "ResourceMonitorService"
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
Stop-Service "ResourceMonitorService"
|
||||||
|
|
||||||
|
# Get service status
|
||||||
|
Get-Service "ResourceMonitorService"
|
||||||
|
|
||||||
|
# Restart the service
|
||||||
|
Restart-Service "ResourceMonitorService"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
For development and testing:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run in development mode with hot reload
|
||||||
|
dotnet run --environment Development
|
||||||
|
|
||||||
|
# Access the dashboard at:
|
||||||
|
# - Development: http://localhost:5000
|
||||||
|
# - Release/Production: http://localhost:24142
|
||||||
|
# Access Swagger API documentation at the same URL + /swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
- **Service won't start**: Check the logs in the `logs/` directory
|
||||||
|
- **No GPU data**: Make sure you have an NVIDIA GPU and drivers installed
|
||||||
|
- **High CPU usage**: Adjust monitoring intervals in `appsettings.json`
|
||||||
|
- **Web dashboard not accessible**: Verify firewall settings and ensure the appropriate port is available (5000 for development, 24142 for release/production)
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
Configuration is managed through `appsettings.json`:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game Detection Configuration
|
||||||
|
|
||||||
|
The service supports advanced game detection through two complementary approaches:
|
||||||
|
|
||||||
|
#### **Platform-Based Detection**
|
||||||
|
Automatically detects games installed through popular game platforms:
|
||||||
|
- **Steam**: Games in `\steamapps\common\` directories
|
||||||
|
- **Epic Games Store**: Games in `\Epic Games\` directories
|
||||||
|
- **GOG Galaxy**: Games in `\GOG Galaxy\Games\` directories
|
||||||
|
- **EA Origin**: Games in `\Origin Games\` directories
|
||||||
|
- **Ubisoft Connect**: Games in `\Ubisoft Game Launcher\games\` directories
|
||||||
|
|
||||||
|
#### **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
@@ -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)
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -1,227 +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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:24142"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-5
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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."
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>">
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user