Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f64ace8a7 | |||
| 3b3bdf3d46 | |||
| 823e467078 | |||
| 294438145a | |||
| 413360ece2 | |||
| a0b9f05ae3 | |||
| caa7436d51 | |||
| 5eec358b68 | |||
| 7c1cbb44f8 | |||
| 06ea991a6c | |||
| 156ae148a7 |
@@ -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,76 @@
|
|||||||
|
namespace ResourceMonitorService.Configuration
|
||||||
|
{
|
||||||
|
public class MonitoringSettings
|
||||||
|
{
|
||||||
|
public int UpdateIntervalMs { get; set; } = 5000; // 5 seconds
|
||||||
|
public int DataRetentionDays { get; set; } = 7;
|
||||||
|
public bool EnableGpuMonitoring { get; set; } = true;
|
||||||
|
public bool EnableDiskMonitoring { get; set; } = true;
|
||||||
|
public bool EnableNetworkMonitoring { get; set; } = true;
|
||||||
|
public bool EnableTemperatureMonitoring { get; set; } = true;
|
||||||
|
public bool EnableProcessMonitoring { get; set; } = true;
|
||||||
|
public bool EnableGameDetection { get; set; } = true;
|
||||||
|
public bool EnableAlerts { get; set; } = true;
|
||||||
|
public int MaxProcessesToTrack { get; set; } = 10;
|
||||||
|
public int MaxHistoryPoints { get; set; } = 1000;
|
||||||
|
|
||||||
|
public List<string> GamePlatformPaths { get; set; } = new()
|
||||||
|
{
|
||||||
|
@"\steamapps\common\",
|
||||||
|
@"\Epic Games\",
|
||||||
|
@"\GOG Galaxy\Games\",
|
||||||
|
@"\Origin Games\",
|
||||||
|
@"\Ubisoft Game Launcher\games\"
|
||||||
|
};
|
||||||
|
|
||||||
|
public List<string> GameRootFolders { get; set; } = new()
|
||||||
|
{
|
||||||
|
@"C:\Games",
|
||||||
|
@"D:\Games",
|
||||||
|
@"E:\Games"
|
||||||
|
};
|
||||||
|
|
||||||
|
public List<AlertThresholdConfig> AlertThresholds { get; set; } = new()
|
||||||
|
{
|
||||||
|
new() { Component = "CPU", WarningThreshold = 80, CriticalThreshold = 95, DurationSeconds = 30 },
|
||||||
|
new() { Component = "Memory", WarningThreshold = 85, CriticalThreshold = 95, DurationSeconds = 30 },
|
||||||
|
new() { Component = "GPU", WarningThreshold = 85, CriticalThreshold = 95, DurationSeconds = 30 },
|
||||||
|
new() { Component = "CPUTemp", WarningThreshold = 75, CriticalThreshold = 85, DurationSeconds = 60 },
|
||||||
|
new() { Component = "GPUTemp", WarningThreshold = 80, CriticalThreshold = 90, DurationSeconds = 60 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ResourceMonitorService.Models
|
||||||
|
{
|
||||||
|
public class SystemInfo
|
||||||
|
{
|
||||||
|
public string MachineName { get; set; } = string.Empty;
|
||||||
|
public string OSVersion { get; set; } = string.Empty;
|
||||||
|
public string OSArchitecture { get; set; } = string.Empty;
|
||||||
|
public int ProcessorCount { get; set; }
|
||||||
|
public ulong TotalPhysicalMemory { get; set; }
|
||||||
|
public string CPUName { get; set; } = string.Empty;
|
||||||
|
public DateTime BootTime { get; set; }
|
||||||
|
public TimeSpan Uptime { get; set; }
|
||||||
|
public string Domain { get; set; } = string.Empty;
|
||||||
|
public bool IsVirtualMachine { get; set; }
|
||||||
|
public string HypervisorVendor { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResourceUsage
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public CpuUsage CPU { get; set; } = new();
|
||||||
|
public MemoryUsage Memory { get; set; } = new();
|
||||||
|
public GpuUsage GPU { get; set; } = new();
|
||||||
|
public List<DiskUsage> Disks { get; set; } = new();
|
||||||
|
public NetworkUsage Network { get; set; } = new();
|
||||||
|
public List<ProcessInfo> TopProcesses { get; set; } = new();
|
||||||
|
public TemperatureInfo Temperature { get; set; } = new();
|
||||||
|
public GameInfo? RunningGame { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CpuUsage
|
||||||
|
{
|
||||||
|
public float Usage { get; set; }
|
||||||
|
public float[] CoreUsages { get; set; } = Array.Empty<float>();
|
||||||
|
public float Temperature { get; set; }
|
||||||
|
public float MaxFrequency { get; set; }
|
||||||
|
public float CurrentFrequency { get; set; }
|
||||||
|
public bool IsThrottling { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MemoryUsage
|
||||||
|
{
|
||||||
|
public float UsagePercentage { get; set; }
|
||||||
|
public ulong UsedMemory { get; set; }
|
||||||
|
public ulong AvailableMemory { get; set; }
|
||||||
|
public ulong TotalMemory { get; set; }
|
||||||
|
public ulong CommittedMemory { get; set; }
|
||||||
|
public ulong PagedMemory { get; set; }
|
||||||
|
public ulong NonPagedMemory { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpuUsage
|
||||||
|
{
|
||||||
|
public uint Usage { get; set; }
|
||||||
|
public uint MemoryUsage { get; set; }
|
||||||
|
public uint Temperature { get; set; }
|
||||||
|
public uint FanSpeed { get; set; }
|
||||||
|
public uint PowerUsage { get; set; }
|
||||||
|
public ulong MemoryTotal { get; set; }
|
||||||
|
public ulong MemoryUsed { get; set; }
|
||||||
|
public bool IsAvailable { get; set; }
|
||||||
|
public string Error { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DriverVersion { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiskUsage
|
||||||
|
{
|
||||||
|
public string DriveLetter { get; set; } = string.Empty;
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public string FileSystem { get; set; } = string.Empty;
|
||||||
|
public ulong TotalSize { get; set; }
|
||||||
|
public ulong FreeSpace { get; set; }
|
||||||
|
public ulong UsedSpace { get; set; }
|
||||||
|
public float UsagePercentage { get; set; }
|
||||||
|
public float ReadSpeed { get; set; } // MB/s
|
||||||
|
public float WriteSpeed { get; set; } // MB/s
|
||||||
|
public float DiskTime { get; set; } // % disk time
|
||||||
|
public uint Temperature { get; set; }
|
||||||
|
public bool IsSSD { get; set; }
|
||||||
|
public long ReadOperations { get; set; }
|
||||||
|
public long WriteOperations { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NetworkUsage
|
||||||
|
{
|
||||||
|
public float UploadSpeed { get; set; } // MB/s
|
||||||
|
public float DownloadSpeed { get; set; } // MB/s
|
||||||
|
public ulong BytesSent { get; set; }
|
||||||
|
public ulong BytesReceived { get; set; }
|
||||||
|
public List<NetworkAdapter> Adapters { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NetworkAdapter
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public bool IsOperational { get; set; }
|
||||||
|
public long Speed { get; set; }
|
||||||
|
public float UploadSpeed { get; set; }
|
||||||
|
public float DownloadSpeed { get; set; }
|
||||||
|
public string IPAddress { get; set; } = string.Empty;
|
||||||
|
public string MACAddress { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProcessInfo
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public float CpuUsage { get; set; }
|
||||||
|
public ulong MemoryUsage { get; set; }
|
||||||
|
public float MemoryUsagePercentage { get; set; }
|
||||||
|
public TimeSpan ProcessorTime { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public string ExecutablePath { get; set; } = string.Empty;
|
||||||
|
public string CommandLine { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemperatureInfo
|
||||||
|
{
|
||||||
|
public float CPU { get; set; }
|
||||||
|
public float GPU { get; set; }
|
||||||
|
public float Motherboard { get; set; }
|
||||||
|
public List<HardDriveTemp> HardDrives { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HardDriveTemp
|
||||||
|
{
|
||||||
|
public string Drive { get; set; } = string.Empty;
|
||||||
|
public float Temperature { get; set; }
|
||||||
|
public string Health { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GameInfo
|
||||||
|
{
|
||||||
|
public string GameName { get; set; } = string.Empty;
|
||||||
|
public string ExecutableName { get; set; } = string.Empty;
|
||||||
|
public string FullPath { get; set; } = string.Empty;
|
||||||
|
public int ProcessId { get; set; }
|
||||||
|
public ulong MemoryUsage { get; set; }
|
||||||
|
public TimeSpan CpuTime { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public string Platform { get; set; } = string.Empty; // Steam, Epic, etc.
|
||||||
|
public bool IsFullscreen { get; set; }
|
||||||
|
public float FPS { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlertThreshold
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Component { get; set; } = string.Empty; // CPU, Memory, GPU, Disk, Network
|
||||||
|
public float WarningThreshold { get; set; }
|
||||||
|
public float CriticalThreshold { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public TimeSpan Duration { get; set; } // How long the threshold must be exceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Alert
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string Component { get; set; } = string.Empty;
|
||||||
|
public string Level { get; set; } = string.Empty; // Warning, Critical
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public float CurrentValue { get; set; }
|
||||||
|
public float ThresholdValue { get; set; }
|
||||||
|
public bool IsResolved { get; set; }
|
||||||
|
public DateTime? ResolvedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+43
-1
@@ -1,5 +1,9 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using ResourceMonitorService.Configuration;
|
||||||
|
using ResourceMonitorService.Services;
|
||||||
|
using Serilog;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace ResourceMonitorService
|
namespace ResourceMonitorService
|
||||||
@@ -8,20 +12,58 @@ namespace ResourceMonitorService
|
|||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// 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();
|
CreateHostBuilder(args).Build().Run();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Fatal(ex, "Application start-up failed");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||||
{
|
{
|
||||||
var builder = Host.CreateDefaultBuilder(args)
|
var builder = Host.CreateDefaultBuilder(args)
|
||||||
|
.UseSerilog()
|
||||||
.ConfigureServices((hostContext, services) =>
|
.ConfigureServices((hostContext, services) =>
|
||||||
{
|
{
|
||||||
|
// Bind configuration sections
|
||||||
|
services.Configure<MonitoringSettings>(
|
||||||
|
hostContext.Configuration.GetSection("MonitoringSettings"));
|
||||||
|
services.Configure<ApiSettings>(
|
||||||
|
hostContext.Configuration.GetSection("ApiSettings"));
|
||||||
|
services.Configure<LoggingSettings>(
|
||||||
|
hostContext.Configuration.GetSection("LoggingSettings"));
|
||||||
|
|
||||||
|
// Register services
|
||||||
|
services.AddSingleton<ISystemInfoService, SystemInfoService>();
|
||||||
|
services.AddSingleton<IResourceMonitorService, Services.ResourceMonitorService>();
|
||||||
|
services.AddSingleton<IGameDetectionService, GameDetectionService>();
|
||||||
|
services.AddSingleton<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,424 @@
|
|||||||
# dotnet
|
# Resource Monitor Service for Unraid VM
|
||||||
|
|
||||||
dotnet run
|
A comprehensive system monitoring service specifically designed for Windows VMs running on Unraid servers. This service provides real-time monitoring of CPU, memory, GPU, disk, network, and system resources through a RESTful API.
|
||||||
git add .
|
|
||||||
git commit -m "Add steam running games"
|
## 🚀 Features
|
||||||
git push origin master
|
|
||||||
dotnet publish -c Release -o ./publish
|
### 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
|
||||||
|
- **System Control**: Remote shutdown/restart capabilities
|
||||||
|
- **Health Monitoring**: Comprehensive health checks and uptime tracking
|
||||||
|
- **Real-time Metrics**: CPU usage calculation and memory percentage tracking for processes
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
The service runs on `http://localhost:5000` by default and provides the following endpoints:
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
- `GET /api/system-info` - Complete system information including VM details
|
||||||
|
- `GET /api/vm/info` - VM-specific information (hypervisor, uptime, etc.)
|
||||||
|
- `GET /api/health` - Service health status and monitoring capabilities
|
||||||
|
- `GET /api/metrics` - Service metrics and performance overview
|
||||||
|
- `GET /api/config` - Current configuration settings
|
||||||
|
|
||||||
|
### Resource Monitoring
|
||||||
|
- `GET /api/resource-usage` - Complete resource usage overview
|
||||||
|
- `GET /api/cpu-usage` - Detailed CPU metrics with per-core data
|
||||||
|
- `GET /api/memory-usage` - Memory utilization and statistics
|
||||||
|
- `GET /api/gpu-usage` - NVIDIA GPU usage, memory, temperature, fan speed, and power consumption
|
||||||
|
- `GET /api/disk-usage` - Disk I/O and space usage for all drives
|
||||||
|
- `GET /api/network-usage` - Network interface statistics
|
||||||
|
- `GET /api/top-processes?count=10` - Top processes by CPU/memory usage with percentage data
|
||||||
|
|
||||||
|
### Game Detection
|
||||||
|
- `GET /api/current-game` - Currently running game information
|
||||||
|
- `GET /api/all-games` - All detected games on the system
|
||||||
|
- `GET /api/fullscreen-status` - Check if any game is running fullscreen
|
||||||
|
|
||||||
|
### Alerting System
|
||||||
|
- `GET /api/alerts/active` - Currently active alerts
|
||||||
|
- `GET /api/alerts/history?count=100` - Alert history
|
||||||
|
- `POST /api/alerts/{alertId}/resolve` - Manually resolve an alert
|
||||||
|
- `GET /api/alerts/enabled` - Check if alerting is enabled
|
||||||
|
|
||||||
|
### System Control
|
||||||
|
- `POST /api/process/kill` - Terminate a process (requires process ID and optional force flag)
|
||||||
|
- `POST /api/system/shutdown` - Shutdown, restart, or cancel system operations
|
||||||
|
- `POST /api/service/stop` - Stop the monitoring service
|
||||||
|
|
||||||
|
**Process Management Details:**
|
||||||
|
- The `/api/top-processes` endpoint returns processes sorted by CPU usage
|
||||||
|
- Each process includes real-time CPU usage percentage and memory usage percentage
|
||||||
|
- CPU usage is calculated using time-based measurements between API calls
|
||||||
|
- Memory usage percentage is calculated relative to total system memory
|
||||||
|
- Process termination supports both graceful (`force: false`) and forced (`force: true`) termination
|
||||||
|
|
||||||
|
## 🛠️ Installation & Usage
|
||||||
|
|
||||||
|
### Option 1: Console Application (Development/Testing)
|
||||||
|
```powershell
|
||||||
|
cd C:\Work\DEV\ResourceUsageAPI
|
||||||
|
dotnet run --configuration Release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Windows Service (Production)
|
||||||
|
```powershell
|
||||||
|
# Run as Administrator
|
||||||
|
cd C:\Work\DEV\ResourceUsageAPI\publish
|
||||||
|
.\install-service.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Standalone Executable
|
||||||
|
```powershell
|
||||||
|
cd C:\Work\DEV\ResourceUsageAPI\publish
|
||||||
|
.\ResourceMonitorService.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
Configuration is managed through `appsettings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MonitoringSettings": {
|
||||||
|
"UpdateIntervalMs": 5000,
|
||||||
|
"EnableGpuMonitoring": true,
|
||||||
|
"EnableDiskMonitoring": true,
|
||||||
|
"EnableNetworkMonitoring": true,
|
||||||
|
"EnableTemperatureMonitoring": true,
|
||||||
|
"EnableProcessMonitoring": true,
|
||||||
|
"EnableGameDetection": true,
|
||||||
|
"EnableAlerts": true,
|
||||||
|
"GamePlatformPaths": [
|
||||||
|
"\\steamapps\\common\\",
|
||||||
|
"\\Epic Games\\",
|
||||||
|
"\\GOG Galaxy\\Games\\",
|
||||||
|
"\\Origin Games\\",
|
||||||
|
"\\Ubisoft Game Launcher\\games\\"
|
||||||
|
],
|
||||||
|
"GameRootFolders": [
|
||||||
|
"C:\\Games",
|
||||||
|
"D:\\Games",
|
||||||
|
"E:\\Games"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AlertThresholds": {
|
||||||
|
"CpuUsageThreshold": 80.0,
|
||||||
|
"MemoryUsageThreshold": 85.0,
|
||||||
|
"GpuUsageThreshold": 90.0,
|
||||||
|
"DiskUsageThreshold": 90.0,
|
||||||
|
"TemperatureThreshold": 80.0,
|
||||||
|
"AlertDurationSeconds": 30
|
||||||
|
},
|
||||||
|
"ApiSettings": {
|
||||||
|
"RequireApiKey": false,
|
||||||
|
"AllowedOrigins": ["http://localhost:4200", "http://unraid:4200"],
|
||||||
|
"BasePath": "/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## 📊 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
|
||||||
|
# Get system health
|
||||||
|
$health = Invoke-RestMethod -Uri "http://localhost:5000/api/health"
|
||||||
|
Write-Host "System Status: $($health.status)"
|
||||||
|
|
||||||
|
# Get CPU usage
|
||||||
|
$cpu = Invoke-RestMethod -Uri "http://localhost:5000/api/cpu-usage"
|
||||||
|
Write-Host "CPU Usage: $($cpu.usage)%"
|
||||||
|
|
||||||
|
# Get GPU usage
|
||||||
|
$gpu = Invoke-RestMethod -Uri "http://localhost:5000/api/gpu-usage"
|
||||||
|
if ($gpu.isAvailable) {
|
||||||
|
Write-Host "GPU: $($gpu.name)"
|
||||||
|
Write-Host "GPU Usage: $($gpu.usage)%"
|
||||||
|
Write-Host "GPU Memory: $([math]::Round($gpu.memoryUsed / 1GB, 2))GB / $([math]::Round($gpu.memoryTotal / 1GB, 2))GB ($($gpu.memoryUsage)%)"
|
||||||
|
Write-Host "GPU Temperature: $($gpu.temperature)°C"
|
||||||
|
} else {
|
||||||
|
Write-Host "GPU not available: $($gpu.error)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current game (enhanced with root folder detection)
|
||||||
|
$game = Invoke-RestMethod -Uri "http://localhost:5000/api/current-game"
|
||||||
|
if ($game) {
|
||||||
|
Write-Host "Currently playing: $($game.gameName)"
|
||||||
|
Write-Host "Platform: $($game.platform)"
|
||||||
|
Write-Host "Executable: $($game.executableName)"
|
||||||
|
if ($game.isFullscreen) {
|
||||||
|
Write-Host "Running in fullscreen mode"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "No game currently detected"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all detected games
|
||||||
|
$allGames = Invoke-RestMethod -Uri "http://localhost:5000/api/all-games"
|
||||||
|
Write-Host "Detected games on system:"
|
||||||
|
foreach ($gameItem in $allGames) {
|
||||||
|
Write-Host " $($gameItem.gameName) ($($gameItem.platform)) - Memory: $([math]::Round($gameItem.memoryUsage / 1MB, 0))MB"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get top processes by CPU usage
|
||||||
|
$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/top-processes?count=5"
|
||||||
|
Write-Host "Top 5 processes by CPU usage:"
|
||||||
|
foreach ($proc in $processes.value) {
|
||||||
|
Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F2'))% CPU, $($proc.memoryUsagePercentage.ToString('F2'))% Memory"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Terminate a process (example - be careful!)
|
||||||
|
$killRequest = @{
|
||||||
|
ProcessId = 1234
|
||||||
|
Force = $false
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
# Invoke-RestMethod -Uri "http://localhost:5000/api/process/kill" -Method Post -Body $killRequest -ContentType "application/json"
|
||||||
|
|
||||||
|
# Shutdown system with 60-second delay
|
||||||
|
$shutdownRequest = @{
|
||||||
|
Action = "shutdown"
|
||||||
|
DelaySeconds = 60
|
||||||
|
Message = "Scheduled maintenance shutdown"
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:5000/api/system/shutdown" -Method Post -Body $shutdownRequest -ContentType "application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Known Warnings (Non-Critical)
|
||||||
|
|
||||||
|
The service may show warnings in VM environments that don't affect functionality:
|
||||||
|
|
||||||
|
- **Performance Counter Warnings**: Some performance counters may not be available in VMs
|
||||||
|
- **Temperature Sensor Access**: Some temperature sensors require elevated privileges
|
||||||
|
- **Process Access Denied**: Some system processes require elevated privileges to access
|
||||||
|
- **Windows.Forms Compatibility**: Game detection works despite .NET Framework compatibility warnings
|
||||||
|
|
||||||
|
These warnings are expected in VM environments and the service continues to function normally.
|
||||||
|
|
||||||
|
## 🎯 Perfect for Unraid
|
||||||
|
|
||||||
|
This service is specifically optimized for Windows VMs running on Unraid:
|
||||||
|
|
||||||
|
- **VM Detection**: Automatically detects and reports virtualization status
|
||||||
|
- **Resource Monitoring**: Tracks VM resource allocation and usage
|
||||||
|
- **Gaming Support**: Detects games and monitors performance impact
|
||||||
|
- **Remote Management**: Full API control for integration with Unraid dashboard
|
||||||
|
- **Alert System**: Configurable alerts for resource thresholds
|
||||||
|
- **Health Monitoring**: Comprehensive health checks for VM status
|
||||||
|
|
||||||
|
## 📝 Logging
|
||||||
|
|
||||||
|
The service uses Serilog for structured logging:
|
||||||
|
- Console output for real-time monitoring
|
||||||
|
- File logging for persistent records
|
||||||
|
- Configurable log levels (Debug, Information, Warning, Error)
|
||||||
|
- Smart error suppression to prevent log spam in VM environments
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
- Optional API key authentication
|
||||||
|
- CORS support for web dashboard integration
|
||||||
|
- Process termination requires explicit API calls
|
||||||
|
- System shutdown/restart requires explicit API calls
|
||||||
|
- Configurable allowed origins for API access
|
||||||
|
|
||||||
|
## 📈 Performance
|
||||||
|
|
||||||
|
- Lightweight background monitoring (5-second intervals by default)
|
||||||
|
- Efficient memory usage with smart caching and cleanup of old process data
|
||||||
|
- Non-blocking async operations
|
||||||
|
- Real-time CPU usage calculation for individual processes
|
||||||
|
- Graceful error handling for VM-specific limitations
|
||||||
|
- Configurable monitoring intervals and features
|
||||||
|
- Smart process tracking with automatic cleanup to prevent memory leaks
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the console output for warnings/errors
|
||||||
|
2. Review the configuration in `appsettings.json`
|
||||||
|
3. Test individual API endpoints using PowerShell or curl
|
||||||
|
4. Check Windows Event Logs if running as a service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 2.1.0
|
||||||
|
**Target Framework**: .NET 9.0
|
||||||
|
**Platforms**: Windows (VM optimized)
|
||||||
|
**License**: Open Source
|
||||||
|
|
||||||
|
### Recent Updates
|
||||||
|
- **v2.1.0**: Added configurable game root folders for enhanced standalone game detection
|
||||||
|
- **v2.0.0**: Initial release with comprehensive system monitoring and game detection
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-ResourceMonitorService-ff17df27-9a94-433d-84e9-744dd4b626c2</UserSecretsId>
|
<UserSecretsId>dotnet-ResourceMonitorService-ff17df27-9a94-433d-84e9-744dd4b626c2</UserSecretsId>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -13,5 +14,8 @@
|
|||||||
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
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 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)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,521 @@
|
|||||||
|
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 screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
|
||||||
|
var screenHeight = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;
|
||||||
|
|
||||||
|
// Check if window covers the entire screen
|
||||||
|
var windowWidth = rect.Right - rect.Left;
|
||||||
|
var windowHeight = rect.Bottom - rect.Top;
|
||||||
|
|
||||||
|
return windowWidth >= screenWidth && windowHeight >= screenHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not determine if game is running fullscreen");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetGameFpsAsync(string processName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
// This is a simplified FPS detection - in reality, you'd need more sophisticated methods
|
||||||
|
// such as hooking into DirectX/OpenGL or using external tools like RTSS
|
||||||
|
|
||||||
|
// For now, we'll return 0 as a placeholder
|
||||||
|
// In a real implementation, you might:
|
||||||
|
// 1. Use Windows Performance Toolkit (WPT) ETW events
|
||||||
|
// 2. Hook into D3D11/D3D12 present calls
|
||||||
|
// 3. Use NVIDIA's NVAPI or AMD's ADL
|
||||||
|
// 4. Parse log files from games that output FPS
|
||||||
|
|
||||||
|
return 0f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not get FPS for process {ProcessName}", processName);
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameInfo? DetectGameFromPath(string filePath, Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First check configured game root folders
|
||||||
|
var gameFromRootFolder = DetectGameFromRootFolders(filePath, process);
|
||||||
|
if (gameFromRootFolder != null)
|
||||||
|
{
|
||||||
|
return gameFromRootFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each configured game platform path
|
||||||
|
foreach (var platformPath in _settings.GamePlatformPaths)
|
||||||
|
{
|
||||||
|
if (filePath.Contains(platformPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var platform = GetPlatformFromPath(platformPath);
|
||||||
|
var gameName = ExtractGameNameFromPath(filePath, platformPath);
|
||||||
|
|
||||||
|
return new GameInfo
|
||||||
|
{
|
||||||
|
GameName = gameName,
|
||||||
|
ExecutableName = Path.GetFileName(filePath),
|
||||||
|
FullPath = filePath,
|
||||||
|
ProcessId = process.Id,
|
||||||
|
MemoryUsage = (ulong)process.WorkingSet64,
|
||||||
|
CpuTime = process.TotalProcessorTime,
|
||||||
|
StartTime = process.StartTime,
|
||||||
|
Platform = platform,
|
||||||
|
IsFullscreen = false, // Will be set by caller
|
||||||
|
FPS = 0f // Will be set by caller
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional checks for common game launchers and executables
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
|
||||||
|
|
||||||
|
// Exclude known system processes and applications
|
||||||
|
var systemExclusions = new[]
|
||||||
|
{
|
||||||
|
"officeclicktorun", "winword", "excel", "powerpoint", "outlook",
|
||||||
|
"teams", "skype", "chrome", "firefox", "edge", "explorer",
|
||||||
|
"notepad", "calculator", "cmd", "powershell", "taskmgr",
|
||||||
|
"svchost", "dwm", "csrss", "winlogon", "lsass", "services",
|
||||||
|
"wininit", "audiodg", "conhost", "rundll32", "msiexec",
|
||||||
|
"setup", "installer", "update", "vshost", "devenv"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip if it's a known system process
|
||||||
|
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownGameExecutables = new[]
|
||||||
|
{
|
||||||
|
"game", "launcher", "client"
|
||||||
|
// Removed generic terms like "main", "start", "run" that match too many system processes
|
||||||
|
};
|
||||||
|
|
||||||
|
var gameIndicators = new[]
|
||||||
|
{
|
||||||
|
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
|
||||||
|
"steam", "epic", "origin", "uplay", "battle.net"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if it's likely a game based on executable name or path
|
||||||
|
// Made the condition more restrictive to reduce false positives
|
||||||
|
if ((knownGameExecutables.Any(exe => fileName.Equals(exe) || fileName.StartsWith(exe + ".")) ||
|
||||||
|
gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase))) &&
|
||||||
|
!filePath.Contains("Program Files\\Common Files", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Try to determine platform and game name from other indicators
|
||||||
|
var platform = DeterminePlatformFromProcess(process, filePath);
|
||||||
|
var gameName = DetermineGameNameFromProcess(process, filePath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(gameName))
|
||||||
|
{
|
||||||
|
return new GameInfo
|
||||||
|
{
|
||||||
|
GameName = gameName,
|
||||||
|
ExecutableName = Path.GetFileName(filePath),
|
||||||
|
FullPath = filePath,
|
||||||
|
ProcessId = process.Id,
|
||||||
|
MemoryUsage = (ulong)process.WorkingSet64,
|
||||||
|
CpuTime = process.TotalProcessorTime,
|
||||||
|
StartTime = process.StartTime,
|
||||||
|
Platform = platform,
|
||||||
|
IsFullscreen = false,
|
||||||
|
FPS = 0f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error detecting game from path {FilePath}", filePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPlatformFromPath(string platformPath)
|
||||||
|
{
|
||||||
|
return platformPath.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
var path when path.Contains("steamapps") => "Steam",
|
||||||
|
var path when path.Contains("epic games") => "Epic Games Store",
|
||||||
|
var path when path.Contains("gog galaxy") => "GOG Galaxy",
|
||||||
|
var path when path.Contains("origin games") => "EA Origin",
|
||||||
|
var path when path.Contains("ubisoft game launcher") => "Ubisoft Connect",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractGameNameFromPath(string filePath, string platformPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parts = filePath.Split(new[] { platformPath }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length > 1)
|
||||||
|
{
|
||||||
|
var gamePath = parts[1];
|
||||||
|
var gameFolder = gamePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||||
|
return gameFolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not extract game name from path {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DeterminePlatformFromProcess(Process process, string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check parent processes for launcher indicators
|
||||||
|
var currentProcess = process;
|
||||||
|
for (int i = 0; i < 3; i++) // Check up to 3 levels up
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parentId = GetParentProcessId(currentProcess.Id);
|
||||||
|
if (parentId == 0) break;
|
||||||
|
|
||||||
|
var parentProcess = Process.GetProcessById(parentId);
|
||||||
|
var parentName = parentProcess.ProcessName.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (parentName.Contains("steam"))
|
||||||
|
return "Steam";
|
||||||
|
if (parentName.Contains("epic"))
|
||||||
|
return "Epic Games Store";
|
||||||
|
if (parentName.Contains("origin"))
|
||||||
|
return "EA Origin";
|
||||||
|
if (parentName.Contains("uplay") || parentName.Contains("ubisoft"))
|
||||||
|
return "Ubisoft Connect";
|
||||||
|
if (parentName.Contains("gog"))
|
||||||
|
return "GOG Galaxy";
|
||||||
|
|
||||||
|
currentProcess = parentProcess;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check file path for platform indicators
|
||||||
|
if (filePath.Contains("Program Files (x86)"))
|
||||||
|
return "Windows Store/Other";
|
||||||
|
if (filePath.Contains("WindowsApps"))
|
||||||
|
return "Microsoft Store";
|
||||||
|
|
||||||
|
return "Standalone";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not determine platform for process {ProcessName}", process.ProcessName);
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DetermineGameNameFromProcess(Process process, string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to get a meaningful name from various sources
|
||||||
|
|
||||||
|
// 1. Try from file properties
|
||||||
|
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
||||||
|
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return versionInfo.ProductName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try from directory name
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(directory);
|
||||||
|
if (!string.IsNullOrEmpty(directoryName) &&
|
||||||
|
!directoryName.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!directoryName.Equals("exe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return directoryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to executable name
|
||||||
|
return Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not determine game name for process {ProcessName}", process.ProcessName);
|
||||||
|
return process.ProcessName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameInfo? DetectGameFromRootFolders(string filePath, Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var rootFolder in _settings.GameRootFolders)
|
||||||
|
{
|
||||||
|
if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var gameName = ExtractGameNameFromRootFolder(filePath, rootFolder);
|
||||||
|
|
||||||
|
return new GameInfo
|
||||||
|
{
|
||||||
|
GameName = gameName,
|
||||||
|
ExecutableName = Path.GetFileName(filePath),
|
||||||
|
FullPath = filePath,
|
||||||
|
ProcessId = process.Id,
|
||||||
|
MemoryUsage = (ulong)process.WorkingSet64,
|
||||||
|
CpuTime = process.TotalProcessorTime,
|
||||||
|
StartTime = process.StartTime,
|
||||||
|
Platform = "Standalone", // Games in root folders are typically standalone
|
||||||
|
IsFullscreen = false, // Will be set by caller
|
||||||
|
FPS = 0f // Will be set by caller
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error detecting game from root folders for path {FilePath}", filePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractGameNameFromRootFolder(string filePath, string rootFolder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Remove the root folder from the path to get the relative game path
|
||||||
|
var relativePath = filePath.Substring(rootFolder.Length).TrimStart('\\', '/');
|
||||||
|
|
||||||
|
// Split by directory separator and take the first part as the game folder
|
||||||
|
var pathParts = relativePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (pathParts.Length > 0)
|
||||||
|
{
|
||||||
|
var gameFolder = pathParts[0];
|
||||||
|
|
||||||
|
// If the game folder name is reasonable, use it
|
||||||
|
if (!string.IsNullOrEmpty(gameFolder) &&
|
||||||
|
!gameFolder.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!gameFolder.Equals("exe", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!gameFolder.Equals("data", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return gameFolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get the game name from file properties
|
||||||
|
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
||||||
|
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return versionInfo.ProductName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: use the executable name
|
||||||
|
return Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not extract game name from root folder path {FilePath}", filePath);
|
||||||
|
return Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetParentProcessId(int processId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var searcher = new System.Management.ManagementObjectSearcher(
|
||||||
|
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}");
|
||||||
|
using var collection = searcher.Get();
|
||||||
|
foreach (System.Management.ManagementObject obj in collection)
|
||||||
|
{
|
||||||
|
return Convert.ToInt32(obj["ParentProcessId"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "Could not get parent process ID for {ProcessId}", processId);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,227 +1,484 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Microsoft.AspNetCore.Http;
|
using ResourceMonitorService.Configuration;
|
||||||
using System.Runtime.InteropServices;
|
using ResourceMonitorService.Models;
|
||||||
using System.Management;
|
using ResourceMonitorService.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace ResourceMonitorService
|
namespace ResourceMonitorService
|
||||||
{
|
{
|
||||||
public class Worker : BackgroundService
|
public class Worker : BackgroundService
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<Worker> _logger;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
|
private readonly ISystemInfoService _systemInfoService;
|
||||||
|
private readonly IResourceMonitorService _resourceMonitorService;
|
||||||
|
private readonly IGameDetectionService _gameDetectionService;
|
||||||
|
private readonly IAlertService _alertService;
|
||||||
|
private readonly ApiSettings _apiSettings;
|
||||||
|
private readonly MonitoringSettings _monitoringSettings;
|
||||||
|
|
||||||
public Worker(IHostApplicationLifetime lifetime)
|
public Worker(
|
||||||
|
ILogger<Worker> logger,
|
||||||
|
IHostApplicationLifetime lifetime,
|
||||||
|
ISystemInfoService systemInfoService,
|
||||||
|
IResourceMonitorService resourceMonitorService,
|
||||||
|
IGameDetectionService gameDetectionService,
|
||||||
|
IAlertService alertService,
|
||||||
|
IOptions<ApiSettings> apiSettings,
|
||||||
|
IOptions<MonitoringSettings> monitoringSettings)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
|
_systemInfoService = systemInfoService;
|
||||||
|
_resourceMonitorService = resourceMonitorService;
|
||||||
|
_gameDetectionService = gameDetectionService;
|
||||||
|
_alertService = alertService;
|
||||||
|
_apiSettings = apiSettings.Value;
|
||||||
|
_monitoringSettings = monitoringSettings.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Resource Monitor Service starting...");
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder();
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
|
||||||
|
// Configure CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("AllowAllOrigins",
|
options.AddPolicy("AllowedOrigins", policy =>
|
||||||
builder => builder.AllowAnyOrigin()
|
policy.WithOrigins(_apiSettings.AllowedOrigins.ToArray())
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod());
|
.AllowAnyMethod());
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
// Apply CORS policy to allow all origins
|
|
||||||
app.UseCors("AllowAllOrigins");
|
|
||||||
|
|
||||||
app.MapGet("/api/resource-usage", async context =>
|
// API Key middleware (if enabled)
|
||||||
|
if (_apiSettings.RequireApiKey)
|
||||||
{
|
{
|
||||||
var currentTime = GetCurrentTime();
|
app.Use(async (context, next) =>
|
||||||
|
|
||||||
var computerInfo = GetComputerInfo();
|
|
||||||
var cpuUsage = GetCpuUsage();
|
|
||||||
var ramUsage = GetRamUsage();
|
|
||||||
var gpuUsage = GetGpuUsage();
|
|
||||||
var runningGame = GetCurrentlyRunningGame();
|
|
||||||
|
|
||||||
var resourceUsage = new
|
|
||||||
{
|
{
|
||||||
CurrentTime = currentTime,
|
if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) ||
|
||||||
ComputerInfo = computerInfo,
|
extractedApiKey != _apiSettings.ApiKey)
|
||||||
CPU = cpuUsage,
|
{
|
||||||
RAM = ramUsage,
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
GPU = gpuUsage,
|
await context.Response.WriteAsync("Unauthorized: Invalid API Key");
|
||||||
CurrentlyRunningGame = runningGame
|
return;
|
||||||
};
|
}
|
||||||
|
await next();
|
||||||
var json = JsonConvert.SerializeObject(resourceUsage);
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
await context.Response.WriteAsync(json);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseCors("AllowedOrigins");
|
||||||
|
|
||||||
|
// Enhanced API endpoints
|
||||||
|
ConfigureApiEndpoints(app);
|
||||||
|
|
||||||
|
// Start background monitoring
|
||||||
|
_ = Task.Run(async () => await BackgroundMonitoringLoop(stoppingToken), stoppingToken);
|
||||||
|
|
||||||
|
// Start the web application
|
||||||
_ = app.RunAsync(stoppingToken);
|
_ = app.RunAsync(stoppingToken);
|
||||||
|
|
||||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object GetComputerInfo()
|
private void ConfigureApiEndpoints(WebApplication app)
|
||||||
{
|
{
|
||||||
return new
|
var basePath = _apiSettings.BasePath;
|
||||||
{
|
|
||||||
MachineName = Environment.MachineName,
|
|
||||||
OSVersion = RuntimeInformation.OSDescription,
|
|
||||||
OSArchitecture = RuntimeInformation.OSArchitecture.ToString(),
|
|
||||||
ProcessorCount = Environment.ProcessorCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private object GetCpuUsage()
|
// System information endpoints
|
||||||
{
|
app.MapGet($"{basePath}/system-info", async () =>
|
||||||
#pragma warning disable CA1416 // Validate platform compatibility
|
Results.Ok(await _systemInfoService.GetSystemInfoAsync()));
|
||||||
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
|
// Resource usage endpoints
|
||||||
var usage = cpuCounter.NextValue();
|
app.MapGet($"{basePath}/resource-usage", async () =>
|
||||||
#pragma warning restore CA1416 // Validate platform compatibility
|
Results.Ok(await _resourceMonitorService.GetResourceUsageAsync()));
|
||||||
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
|
app.MapGet($"{basePath}/cpu-usage", async () =>
|
||||||
return new
|
Results.Ok(await _resourceMonitorService.GetCpuUsageAsync()));
|
||||||
{
|
|
||||||
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
|
app.MapGet($"{basePath}/memory-usage", async () =>
|
||||||
|
Results.Ok(await _resourceMonitorService.GetMemoryUsageAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/gpu-usage", async () =>
|
||||||
|
Results.Ok(await _resourceMonitorService.GetGpuUsageAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/disk-usage", async () =>
|
||||||
|
Results.Ok(await _resourceMonitorService.GetDiskUsageAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/network-usage", async () =>
|
||||||
|
Results.Ok(await _resourceMonitorService.GetNetworkUsageAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/top-processes", async (int count = 10) =>
|
||||||
|
Results.Ok(await _resourceMonitorService.GetTopProcessesAsync(count)));
|
||||||
|
|
||||||
|
// Game detection endpoints
|
||||||
|
app.MapGet($"{basePath}/current-game", async () =>
|
||||||
|
Results.Ok(await _gameDetectionService.GetCurrentlyRunningGameAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/all-games", async () =>
|
||||||
|
Results.Ok(await _gameDetectionService.GetAllDetectedGamesAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/fullscreen-status", async () =>
|
||||||
|
Results.Ok(new { IsFullscreen = await _gameDetectionService.IsGameRunningFullscreenAsync() }));
|
||||||
|
|
||||||
|
// Alert endpoints
|
||||||
|
app.MapGet($"{basePath}/alerts/active", async () =>
|
||||||
|
Results.Ok(await _alertService.GetActiveAlertsAsync()));
|
||||||
|
|
||||||
|
app.MapGet($"{basePath}/alerts/history", async (int count = 100) =>
|
||||||
|
Results.Ok(await _alertService.GetAlertHistoryAsync(count)));
|
||||||
|
|
||||||
|
app.MapPost($"{basePath}/alerts/{{alertId}}/resolve", async (string alertId) =>
|
||||||
{
|
{
|
||||||
Usage = usage
|
await _alertService.ResolveAlertAsync(alertId);
|
||||||
};
|
return Results.Ok(new { Message = "Alert resolved successfully" });
|
||||||
}
|
});
|
||||||
|
|
||||||
private float GetRamUsage()
|
app.MapGet($"{basePath}/alerts/enabled", async () =>
|
||||||
{
|
Results.Ok(new { Enabled = await _alertService.IsAlertingEnabledAsync() }));
|
||||||
#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()
|
// Process management endpoints (enhanced)
|
||||||
{
|
app.MapPost($"{basePath}/process/kill", async (HttpContext context) =>
|
||||||
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.
|
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||||
var filePath = process.MainModule.FileName;
|
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
||||||
#pragma warning restore CS8602 // Dereference of a possibly null reference.
|
|
||||||
if (filePath.Contains(@"\steamapps\common\"))
|
int processId = request?.ProcessId ?? 0;
|
||||||
|
bool force = request?.Force ?? false;
|
||||||
|
|
||||||
|
if (processId <= 0)
|
||||||
{
|
{
|
||||||
// Extract the game directory name
|
return Results.BadRequest("Invalid process ID");
|
||||||
var parts = filePath.Split(new[] { @"\steamapps\common\" }, StringSplitOptions.None);
|
}
|
||||||
if (parts.Length > 1)
|
|
||||||
|
var processes = Process.GetProcesses().Where(p => p.Id == processId).ToArray();
|
||||||
|
|
||||||
|
if (processes.Length == 0)
|
||||||
{
|
{
|
||||||
var gamePath = parts[1];
|
return Results.NotFound($"No process found with ID {processId}");
|
||||||
var gameName = gamePath.Split(Path.DirectorySeparatorChar)[0];
|
}
|
||||||
return new
|
|
||||||
|
var process = processes[0];
|
||||||
|
var processName = process.ProcessName;
|
||||||
|
|
||||||
|
if (force)
|
||||||
{
|
{
|
||||||
GameName = gameName,
|
process.Kill(true); // Force kill entire process tree
|
||||||
ExecutableName = Path.GetFileName(filePath),
|
}
|
||||||
FullPath = filePath,
|
else
|
||||||
ProcessId = process.Id,
|
{
|
||||||
MemoryUsage = process.WorkingSet64 / (1024 * 1024) + " MB", // Memory usage in MB
|
process.CloseMainWindow(); // Try graceful close first
|
||||||
CpuTime = process.TotalProcessorTime.ToString(),
|
|
||||||
StartTime = process.StartTime.ToString("G"), // General date/time pattern
|
// Wait a bit for graceful close
|
||||||
UserName = Environment.UserName // The user running the process
|
await Task.Delay(3000);
|
||||||
|
|
||||||
|
if (!process.HasExited)
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("Process {ProcessName} (ID: {ProcessId}) was terminated", processName, processId);
|
||||||
|
return Results.Ok(new { Message = $"Process {processName} (ID: {processId}) has been terminated." });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error terminating process");
|
||||||
|
return Results.Problem(ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced shutdown/restart endpoints
|
||||||
|
app.MapPost($"{basePath}/system/shutdown", async (HttpContext context) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||||
|
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
||||||
|
|
||||||
|
string action = request?.Action?.ToString()?.ToLower() ?? "shutdown";
|
||||||
|
int delaySeconds = request?.DelaySeconds ?? 0;
|
||||||
|
string message = request?.Message?.ToString() ?? "System shutdown initiated by Resource Monitor";
|
||||||
|
|
||||||
|
if (action != "shutdown" && action != "restart" && action != "cancel")
|
||||||
|
{
|
||||||
|
return Results.BadRequest("Invalid action. Use 'shutdown', 'restart', or 'cancel'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == "cancel")
|
||||||
|
{
|
||||||
|
var cancelProcess = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "shutdown",
|
||||||
|
Arguments = "/a",
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false
|
||||||
};
|
};
|
||||||
}
|
Process.Start(cancelProcess);
|
||||||
}
|
|
||||||
}
|
_logger.LogWarning("System shutdown cancelled");
|
||||||
catch (Exception)
|
return Results.Ok(new { Message = "Shutdown cancelled." });
|
||||||
{
|
|
||||||
// Handle access exceptions or continue if not important
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "No Steam game is currently running.";
|
|
||||||
}
|
|
||||||
private string GetCurrentTime()
|
|
||||||
{
|
|
||||||
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delaySeconds < 0)
|
||||||
|
{
|
||||||
|
return Results.BadRequest("Delay must be a non-negative integer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string shutdownCommand = action == "shutdown"
|
||||||
|
? $"/s /f /t {delaySeconds} /c \"{message}\""
|
||||||
|
: $"/r /f /t {delaySeconds} /c \"{message}\"";
|
||||||
|
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "shutdown",
|
||||||
|
Arguments = shutdownCommand,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Process.Start(processStartInfo);
|
||||||
|
|
||||||
|
_logger.LogWarning("System {Action} initiated with {Delay} seconds delay", action, delaySeconds);
|
||||||
|
return Results.Ok(new {
|
||||||
|
Message = $"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds.",
|
||||||
|
Action = action,
|
||||||
|
DelaySeconds = delaySeconds,
|
||||||
|
Timestamp = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error executing system command");
|
||||||
|
return Results.Problem(ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// VM-specific endpoints for Unraid
|
||||||
|
app.MapGet($"{basePath}/vm/info", async () =>
|
||||||
|
{
|
||||||
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
IsVirtualMachine = systemInfo.IsVirtualMachine,
|
||||||
|
HypervisorVendor = systemInfo.HypervisorVendor,
|
||||||
|
Uptime = systemInfo.Uptime,
|
||||||
|
BootTime = systemInfo.BootTime,
|
||||||
|
MachineName = systemInfo.MachineName,
|
||||||
|
Domain = systemInfo.Domain
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Performance history endpoint (simple in-memory storage)
|
||||||
|
app.MapGet($"{basePath}/performance/history", async (int minutes = 60) =>
|
||||||
|
{
|
||||||
|
// This would ideally be stored in a database or time-series database
|
||||||
|
// For now, return current snapshot with timestamp
|
||||||
|
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
||||||
|
return Results.Ok(new {
|
||||||
|
Current = usage,
|
||||||
|
Message = "Historical data not implemented yet - showing current values"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.MapGet($"{basePath}/health", async () =>
|
||||||
|
{
|
||||||
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
||||||
|
var alerts = await _alertService.GetActiveAlertsAsync();
|
||||||
|
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
Status = "Healthy",
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
Uptime = systemInfo.Uptime,
|
||||||
|
ActiveAlerts = alerts.Count,
|
||||||
|
MonitoringEnabled = new
|
||||||
|
{
|
||||||
|
GPU = _monitoringSettings.EnableGpuMonitoring,
|
||||||
|
Disk = _monitoringSettings.EnableDiskMonitoring,
|
||||||
|
Network = _monitoringSettings.EnableNetworkMonitoring,
|
||||||
|
Temperature = _monitoringSettings.EnableTemperatureMonitoring,
|
||||||
|
Processes = _monitoringSettings.EnableProcessMonitoring,
|
||||||
|
Games = _monitoringSettings.EnableGameDetection,
|
||||||
|
Alerts = _monitoringSettings.EnableAlerts
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Service control endpoints
|
||||||
|
app.MapPost($"{basePath}/service/stop", () =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Service stop requested via API");
|
||||||
|
_lifetime.StopApplication();
|
||||||
|
return Results.Ok(new { Message = "Stopping the service..." });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration endpoint
|
||||||
|
app.MapGet($"{basePath}/config", () => Results.Ok(new
|
||||||
|
{
|
||||||
|
MonitoringSettings = new
|
||||||
|
{
|
||||||
|
UpdateInterval = _monitoringSettings.UpdateIntervalMs,
|
||||||
|
EnableGpuMonitoring = _monitoringSettings.EnableGpuMonitoring,
|
||||||
|
EnableDiskMonitoring = _monitoringSettings.EnableDiskMonitoring,
|
||||||
|
EnableNetworkMonitoring = _monitoringSettings.EnableNetworkMonitoring,
|
||||||
|
EnableTemperatureMonitoring = _monitoringSettings.EnableTemperatureMonitoring,
|
||||||
|
EnableProcessMonitoring = _monitoringSettings.EnableProcessMonitoring,
|
||||||
|
EnableGameDetection = _monitoringSettings.EnableGameDetection,
|
||||||
|
EnableAlerts = _monitoringSettings.EnableAlerts
|
||||||
|
},
|
||||||
|
ApiSettings = new
|
||||||
|
{
|
||||||
|
BasePath = _apiSettings.BasePath,
|
||||||
|
RequireApiKey = _apiSettings.RequireApiKey,
|
||||||
|
AllowedOrigins = _apiSettings.AllowedOrigins
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Service metrics endpoint
|
||||||
|
app.MapGet($"{basePath}/metrics", async () =>
|
||||||
|
{
|
||||||
|
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
||||||
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
Service = "Resource Monitor Service",
|
||||||
|
Version = "2.0.0",
|
||||||
|
Status = "Running",
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
Uptime = systemInfo.Uptime,
|
||||||
|
LastUpdate = usage.Timestamp,
|
||||||
|
Performance = new
|
||||||
|
{
|
||||||
|
CPU = usage.CPU.Usage,
|
||||||
|
Memory = usage.Memory.UsagePercentage,
|
||||||
|
GPU = usage.GPU.Usage,
|
||||||
|
ActiveProcesses = usage.TopProcesses.Count
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Root endpoint
|
||||||
|
app.MapGet("/", () => Results.Ok(new
|
||||||
|
{
|
||||||
|
Service = "Resource Monitor Service for Unraid VM",
|
||||||
|
Version = "2.0.0",
|
||||||
|
Status = "Running",
|
||||||
|
Timestamp = DateTime.Now,
|
||||||
|
ApiBasePath = _apiSettings.BasePath,
|
||||||
|
Endpoints = new[]
|
||||||
|
{
|
||||||
|
$"{_apiSettings.BasePath}/health",
|
||||||
|
$"{_apiSettings.BasePath}/system-info",
|
||||||
|
$"{_apiSettings.BasePath}/resource-usage",
|
||||||
|
$"{_apiSettings.BasePath}/cpu-usage",
|
||||||
|
$"{_apiSettings.BasePath}/memory-usage",
|
||||||
|
$"{_apiSettings.BasePath}/gpu-usage",
|
||||||
|
$"{_apiSettings.BasePath}/disk-usage",
|
||||||
|
$"{_apiSettings.BasePath}/network-usage",
|
||||||
|
$"{_apiSettings.BasePath}/top-processes",
|
||||||
|
$"{_apiSettings.BasePath}/current-game",
|
||||||
|
$"{_apiSettings.BasePath}/alerts/active",
|
||||||
|
$"{_apiSettings.BasePath}/vm/info",
|
||||||
|
$"{_apiSettings.BasePath}/config",
|
||||||
|
$"{_apiSettings.BasePath}/metrics"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
_logger.LogInformation("API endpoints configured. Base path: {BasePath}", _apiSettings.BasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Background monitoring started");
|
||||||
|
int errorCount = 0;
|
||||||
|
int successfulCycles = 0;
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get current resource usage
|
||||||
|
var resourceUsage = await _resourceMonitorService.GetResourceUsageAsync();
|
||||||
|
|
||||||
|
// Add current game info if game detection is enabled
|
||||||
|
if (_monitoringSettings.EnableGameDetection)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resourceUsage.RunningGame = await _gameDetectionService.GetCurrentlyRunningGameAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Only log game detection errors occasionally to avoid spam
|
||||||
|
if (errorCount % 12 == 0) // Every minute if 5-second intervals
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Game detection error (suppressed): {Message}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for alerts
|
||||||
|
if (_monitoringSettings.EnableAlerts)
|
||||||
|
{
|
||||||
|
await _alertService.CheckAndGenerateAlertsAsync(resourceUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulCycles++;
|
||||||
|
|
||||||
|
// Log performance metrics occasionally
|
||||||
|
if (successfulCycles % 6 == 0) // Every 30 seconds with 5-second intervals
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%",
|
||||||
|
resourceUsage.CPU.Usage,
|
||||||
|
resourceUsage.Memory.UsagePercentage,
|
||||||
|
resourceUsage.GPU.Usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful monitoring occasionally for health verification
|
||||||
|
if (successfulCycles % 120 == 0) // Every 10 minutes
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Background monitoring healthy - completed {SuccessfulCycles} cycles", successfulCycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorCount = 0; // Reset error count on success
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Background monitoring stopped after {SuccessfulCycles} successful cycles", successfulCycles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-1
@@ -2,7 +2,10 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information",
|
||||||
|
"ResourceMonitorService.Services.ResourceMonitorService": "Error",
|
||||||
|
"ResourceMonitorService.Services.GameDetectionService": "Error",
|
||||||
|
"System": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RunAsWindowsService": true,
|
"RunAsWindowsService": true,
|
||||||
@@ -12,5 +15,88 @@
|
|||||||
"Url": "http://*:5000"
|
"Url": "http://*:5000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ApiSettings": {
|
||||||
|
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a",
|
||||||
|
"RequireApiKey": false,
|
||||||
|
"AllowedOrigins": [
|
||||||
|
"http://localhost:4200",
|
||||||
|
"http://192.168.50.52:4200",
|
||||||
|
"http://vmwin11:4200",
|
||||||
|
"http://unraid:4200"
|
||||||
|
],
|
||||||
|
"EnableSwagger": false,
|
||||||
|
"BasePath": "/api"
|
||||||
|
},
|
||||||
|
"MonitoringSettings": {
|
||||||
|
"UpdateIntervalMs": 5000,
|
||||||
|
"DataRetentionDays": 7,
|
||||||
|
"EnableGpuMonitoring": true,
|
||||||
|
"EnableDiskMonitoring": true,
|
||||||
|
"EnableNetworkMonitoring": true,
|
||||||
|
"EnableTemperatureMonitoring": true,
|
||||||
|
"EnableProcessMonitoring": true,
|
||||||
|
"EnableGameDetection": true,
|
||||||
|
"EnableAlerts": true,
|
||||||
|
"MaxProcessesToTrack": 10,
|
||||||
|
"MaxHistoryPoints": 1000,
|
||||||
|
"GamePlatformPaths": [
|
||||||
|
"\\steamapps\\common\\",
|
||||||
|
"\\Epic Games\\",
|
||||||
|
"\\GOG Galaxy\\Games\\",
|
||||||
|
"\\Origin Games\\",
|
||||||
|
"\\Ubisoft Game Launcher\\games\\"
|
||||||
|
],
|
||||||
|
"GameRootFolders": [
|
||||||
|
"C:\\Games",
|
||||||
|
"D:\\Games",
|
||||||
|
"E:\\Games"
|
||||||
|
],
|
||||||
|
"AlertThresholds": [
|
||||||
|
{
|
||||||
|
"Component": "CPU",
|
||||||
|
"WarningThreshold": 80,
|
||||||
|
"CriticalThreshold": 95,
|
||||||
|
"DurationSeconds": 30,
|
||||||
|
"IsEnabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Component": "Memory",
|
||||||
|
"WarningThreshold": 85,
|
||||||
|
"CriticalThreshold": 95,
|
||||||
|
"DurationSeconds": 30,
|
||||||
|
"IsEnabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Component": "GPU",
|
||||||
|
"WarningThreshold": 85,
|
||||||
|
"CriticalThreshold": 95,
|
||||||
|
"DurationSeconds": 30,
|
||||||
|
"IsEnabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Component": "CPUTemp",
|
||||||
|
"WarningThreshold": 75,
|
||||||
|
"CriticalThreshold": 85,
|
||||||
|
"DurationSeconds": 60,
|
||||||
|
"IsEnabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Component": "GPUTemp",
|
||||||
|
"WarningThreshold": 80,
|
||||||
|
"CriticalThreshold": 90,
|
||||||
|
"DurationSeconds": 60,
|
||||||
|
"IsEnabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"LoggingSettings": {
|
||||||
|
"LogLevel": "Information",
|
||||||
|
"LogPath": "logs",
|
||||||
|
"MaxLogFiles": 30,
|
||||||
|
"MaxLogFileSizeMB": 10,
|
||||||
|
"EnableFileLogging": true,
|
||||||
|
"EnableConsoleLogging": true,
|
||||||
|
"EnablePerformanceLogging": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Resource Monitor Service - Installation Script for Windows Service
|
||||||
|
# Run this in PowerShell as Administrator
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$Uninstall
|
||||||
|
)
|
||||||
|
|
||||||
|
$SERVICE_NAME = "ResourceMonitorService"
|
||||||
|
$SERVICE_DISPLAY_NAME = "Resource Monitor Service v2.0"
|
||||||
|
$SERVICE_DESCRIPTION = "Monitors VM resources for Unraid integration"
|
||||||
|
$INSTALL_PATH = "C:\Services\ResourceMonitor"
|
||||||
|
|
||||||
|
Write-Host "=== Resource Monitor Service - Windows Service Installer ===" -ForegroundColor Cyan
|
||||||
|
Write-Host
|
||||||
|
|
||||||
|
# Check if running as administrator
|
||||||
|
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||||
|
Write-Host "ERROR: This script must be run as Administrator" -ForegroundColor Red
|
||||||
|
Write-Host "Please run PowerShell as Administrator and try again" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Uninstall) {
|
||||||
|
Write-Host "Uninstalling Resource Monitor Service..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
Write-Host "Stopping service..."
|
||||||
|
try {
|
||||||
|
Stop-Service -Name $SERVICE_NAME -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Service stopped successfully" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "Service was not running" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the service
|
||||||
|
Write-Host "Removing service..."
|
||||||
|
try {
|
||||||
|
sc.exe delete $SERVICE_NAME
|
||||||
|
Write-Host "Service removed successfully" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "Failed to remove service: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove firewall rule
|
||||||
|
Write-Host "Removing firewall rule..."
|
||||||
|
try {
|
||||||
|
Remove-NetFirewallRule -DisplayName "Resource Monitor Service" -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Firewall rule removed" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "Firewall rule not found or already removed" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optionally remove installation directory
|
||||||
|
$removeFiles = Read-Host "Remove installation files from $INSTALL_PATH? (y/N)"
|
||||||
|
if ($removeFiles -eq "y" -or $removeFiles -eq "Y") {
|
||||||
|
try {
|
||||||
|
Remove-Item -Path $INSTALL_PATH -Recurse -Force -ErrorAction Stop
|
||||||
|
Write-Host "Installation files removed" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "Failed to remove installation files: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Uninstallation complete!" -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installing Resource Monitor Service as Windows Service..." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
Write-Host "Creating installation directory..."
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $INSTALL_PATH -Force | Out-Null
|
||||||
|
Write-Host "Installation directory created: $INSTALL_PATH" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "ERROR: Failed to create installation directory: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build the service in release mode
|
||||||
|
Write-Host "Building service..."
|
||||||
|
try {
|
||||||
|
$buildResult = dotnet publish --configuration Release --output $INSTALL_PATH
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Build failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
Write-Host "Service built successfully" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "ERROR: Build failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop existing service if running
|
||||||
|
Write-Host "Stopping existing service (if running)..."
|
||||||
|
try {
|
||||||
|
Stop-Service -Name $SERVICE_NAME -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Existing service stopped" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "No existing service found" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove existing service if it exists
|
||||||
|
Write-Host "Removing existing service (if exists)..."
|
||||||
|
try {
|
||||||
|
sc.exe delete $SERVICE_NAME 2>$null
|
||||||
|
} catch {
|
||||||
|
# Ignore errors if service doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install the service
|
||||||
|
Write-Host "Installing Windows Service..."
|
||||||
|
try {
|
||||||
|
$serviceBinPath = "`"$INSTALL_PATH\ResourceMonitorService.exe`" --windows-service"
|
||||||
|
$createResult = sc.exe create $SERVICE_NAME binPath= $serviceBinPath DisplayName= $SERVICE_DISPLAY_NAME start= auto
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to create service with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set service description
|
||||||
|
sc.exe description $SERVICE_NAME $SERVICE_DESCRIPTION
|
||||||
|
|
||||||
|
Write-Host "Windows Service created successfully" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "ERROR: Failed to create Windows Service: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure service recovery options
|
||||||
|
Write-Host "Configuring service recovery options..."
|
||||||
|
try {
|
||||||
|
sc.exe failure $SERVICE_NAME reset= 300 actions= restart/5000/restart/5000/restart/10000
|
||||||
|
Write-Host "Service recovery options configured" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "WARNING: Failed to configure service recovery options" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure firewall rule
|
||||||
|
Write-Host "Configuring Windows Firewall..."
|
||||||
|
try {
|
||||||
|
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow -Profile Any -ErrorAction Stop
|
||||||
|
Write-Host "Firewall rule created" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "WARNING: Failed to create firewall rule. You may need to configure manually." -ForegroundColor Yellow
|
||||||
|
Write-Host "Manual command: New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
Write-Host "Starting service..."
|
||||||
|
try {
|
||||||
|
Start-Service -Name $SERVICE_NAME
|
||||||
|
Write-Host "Service started successfully" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "ERROR: Failed to start service: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
Write-Host "Check Windows Event Log for details" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait a moment and check service status
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
$serviceStatus = Get-Service -Name $SERVICE_NAME
|
||||||
|
Write-Host "Service Status: $($serviceStatus.Status)" -ForegroundColor $(if ($serviceStatus.Status -eq "Running") { "Green" } else { "Red" })
|
||||||
|
|
||||||
|
Write-Host
|
||||||
|
Write-Host "=== Installation Complete ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "Service Name: $SERVICE_NAME" -ForegroundColor White
|
||||||
|
Write-Host "Installation Path: $INSTALL_PATH" -ForegroundColor White
|
||||||
|
Write-Host "Service URL: http://localhost:5000" -ForegroundColor White
|
||||||
|
Write-Host "API Health Check: http://localhost:5000/api/health" -ForegroundColor White
|
||||||
|
Write-Host
|
||||||
|
Write-Host "The service is now running and will start automatically with Windows." -ForegroundColor Green
|
||||||
|
Write-Host "You can manage it through Services.msc or using PowerShell commands:" -ForegroundColor White
|
||||||
|
Write-Host " - Stop: Stop-Service -Name $SERVICE_NAME" -ForegroundColor Gray
|
||||||
|
Write-Host " - Start: Start-Service -Name $SERVICE_NAME" -ForegroundColor Gray
|
||||||
|
Write-Host " - Status: Get-Service -Name $SERVICE_NAME" -ForegroundColor Gray
|
||||||
|
Write-Host " - Restart: Restart-Service -Name $SERVICE_NAME" -ForegroundColor Gray
|
||||||
|
Write-Host
|
||||||
|
Write-Host "To uninstall: .\install-service.ps1 -Uninstall" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Test the API endpoint
|
||||||
|
Write-Host
|
||||||
|
Write-Host "Testing API endpoint..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri "http://localhost:5000/api/health" -TimeoutSec 10
|
||||||
|
Write-Host "API Test Result: SUCCESS" -ForegroundColor Green
|
||||||
|
Write-Host "Service Version: $($response.Service)" -ForegroundColor White
|
||||||
|
Write-Host "Status: $($response.Status)" -ForegroundColor White
|
||||||
|
} catch {
|
||||||
|
Write-Host "API Test Result: FAILED" -ForegroundColor Red
|
||||||
|
Write-Host "The service may still be starting up. Wait a few minutes and try accessing:" -ForegroundColor Yellow
|
||||||
|
Write-Host "http://localhost:5000/api/health" -ForegroundColor White
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Resource Monitor Service - Installation Script for Windows Service
|
||||||
|
|
||||||
|
SERVICE_NAME="ResourceMonitorService"
|
||||||
|
SERVICE_DISPLAY_NAME="Resource Monitor Service v2.0"
|
||||||
|
SERVICE_DESCRIPTION="Monitors VM resources for Unraid integration"
|
||||||
|
INSTALL_PATH="C:\Services\ResourceMonitor"
|
||||||
|
|
||||||
|
echo "=== Resource Monitor Service - Windows Service Installer ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if running as administrator
|
||||||
|
if [[ ! $(id -u) -eq 0 ]]; then
|
||||||
|
echo "ERROR: This script must be run as Administrator"
|
||||||
|
echo "Please run PowerShell as Administrator and try again"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing Resource Monitor Service as Windows Service..."
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
echo "Creating installation directory..."
|
||||||
|
mkdir -p "$INSTALL_PATH"
|
||||||
|
|
||||||
|
# Build the service in release mode
|
||||||
|
echo "Building service..."
|
||||||
|
dotnet publish --configuration Release --output "$INSTALL_PATH"
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "ERROR: Build failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop existing service if running
|
||||||
|
echo "Stopping existing service (if running)..."
|
||||||
|
sc stop "$SERVICE_NAME" 2>/dev/null
|
||||||
|
|
||||||
|
# Remove existing service if it exists
|
||||||
|
echo "Removing existing service (if exists)..."
|
||||||
|
sc delete "$SERVICE_NAME" 2>/dev/null
|
||||||
|
|
||||||
|
# Install the service
|
||||||
|
echo "Installing Windows Service..."
|
||||||
|
sc create "$SERVICE_NAME" \
|
||||||
|
binPath="\"$INSTALL_PATH\\ResourceMonitorService.exe\" --windows-service" \
|
||||||
|
DisplayName="$SERVICE_DISPLAY_NAME" \
|
||||||
|
Description="$SERVICE_DESCRIPTION" \
|
||||||
|
start=auto
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "ERROR: Failed to create Windows Service"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure service recovery options
|
||||||
|
echo "Configuring service recovery options..."
|
||||||
|
sc failure "$SERVICE_NAME" reset=300 actions=restart/5000/restart/5000/restart/10000
|
||||||
|
|
||||||
|
# Configure firewall rule
|
||||||
|
echo "Configuring Windows Firewall..."
|
||||||
|
powershell -Command "New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow -Profile Any" 2>/dev/null
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
echo "Starting service..."
|
||||||
|
sc start "$SERVICE_NAME"
|
||||||
|
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
echo "=== Installation Complete ==="
|
||||||
|
echo "Service Name: $SERVICE_NAME"
|
||||||
|
echo "Installation Path: $INSTALL_PATH"
|
||||||
|
echo "Service URL: http://localhost:5000"
|
||||||
|
echo "API Health Check: http://localhost:5000/api/health"
|
||||||
|
echo
|
||||||
|
echo "The service is now running and will start automatically with Windows."
|
||||||
|
echo "You can manage it through Services.msc or using sc commands:"
|
||||||
|
echo " - Stop: sc stop $SERVICE_NAME"
|
||||||
|
echo " - Start: sc start $SERVICE_NAME"
|
||||||
|
echo " - Status: sc query $SERVICE_NAME"
|
||||||
|
echo
|
||||||
|
echo "To uninstall: sc stop $SERVICE_NAME && sc delete $SERVICE_NAME"
|
||||||
|
else
|
||||||
|
echo "ERROR: Failed to start service"
|
||||||
|
echo "Check Windows Event Log for details"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# PowerShell script to create a new inbound rule in Windows Firewall
|
# PowerShell script to create a new inbound rule in Windows Firewall
|
||||||
$port = 5000
|
$port = 5000
|
||||||
$ruleName = "ResourceMonitorService"
|
$ruleName = "ResourceMonitorServicePublish"
|
||||||
if (Get-NetFirewallRule -DisplayName $ruleName) {
|
if (Get-NetFirewallRule -DisplayName $ruleName) {
|
||||||
Write-Host "Rule already exists, not creating a new one"
|
Write-Host "Rule already exists, not creating a new one"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sc create ResourceMonitorServicerrr binPath= "%~dp0ResourceMonitorService.exe --windows-service" start= auto
|
sc create ResourceMonitorService binPath="%~dp0ResourceMonitorService.exe --windows-service" start= auto
|
||||||
sc description ResourceMonitorServicerrr "A service that monitors system resource usage and exposes it via a web API."
|
sc description ResourceMonitorService "A service that monitors system resource usage and exposes it via a web API."
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RunAsWindowsService": true,
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiSettings": {
|
||||||
|
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RunAsWindowsService": true,
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiSettings": {
|
||||||
|
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RunAsWindowsService": true,
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiSettings": {
|
||||||
|
"ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@echo off
|
||||||
|
REM Resource Monitor Service - Quick Start Script
|
||||||
|
|
||||||
|
echo Starting Resource Monitor Service v2.0...
|
||||||
|
echo.
|
||||||
|
echo This service will monitor your VM's resources and provide a REST API
|
||||||
|
echo for remote monitoring from your Unraid server.
|
||||||
|
echo.
|
||||||
|
echo Service will be available at: http://localhost:5000
|
||||||
|
echo API Documentation: http://localhost:5000/api/health
|
||||||
|
echo.
|
||||||
|
echo Press Ctrl+C to stop the service
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Change to the script's directory
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
REM Check if .NET 9 is installed
|
||||||
|
dotnet --version > nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: .NET 9.0 Runtime is required but not found.
|
||||||
|
echo Please install .NET 9.0 Runtime from: https://dotnet.microsoft.com/download
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Build and run the service
|
||||||
|
echo Building service...
|
||||||
|
dotnet build --configuration Release
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Build failed. Please check the error messages above.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Starting service on http://localhost:5000
|
||||||
|
echo.
|
||||||
|
dotnet run --configuration Release
|
||||||
|
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user