diff --git a/NvmlWrapper.cs b/NvmlWrapper.cs index 7ebdd45..42297dc 100644 --- a/NvmlWrapper.cs +++ b/NvmlWrapper.cs @@ -9,6 +9,15 @@ public static class NvmlWrapper [DllImport("nvml.dll", EntryPoint = "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")] public static extern int NvmlDeviceGetHandleByIndex(int index, out IntPtr device); diff --git a/README.md b/README.md index 3310347..12fbfe1 100644 --- a/README.md +++ b/README.md @@ -104,4 +104,16 @@ dotnet run git add . git commit -m "Add steam running games" git push origin master -dotnet publish -c Release -o ./publish \ No newline at end of file +dotnet publish -c Release -o ./publish + +# devtest +Invoke-WebRequest -Uri "http://localhost:5000/api/kill-process" -Method POST -Body "1234" -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } + +Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/resource-usage" -Method GET -Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } + +Use 'shutdown', 'restart', or 'cancel'. +Invoke-WebRequest -Uri "http://192.168.50.52:5000/api/force-shutdown" ` +-Method POST ` +-Headers @{ "X-API-KEY" = "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } ` +-Body '{"Action": "shutdown", "DelaySeconds": 120}' ` +-ContentType "application/json" diff --git a/Worker.cs b/Worker.cs index 2fa0983..502b5a7 100644 --- a/Worker.cs +++ b/Worker.cs @@ -25,12 +25,35 @@ namespace ResourceMonitorService builder.Services.AddCors(options => { options.AddPolicy("AllowAllOrigins", - builder => builder.AllowAnyOrigin() + builder => builder + .WithOrigins("http://localhost:4200","http://192.168.50.52:4200","http://vmwin11:4200") .AllowAnyHeader() .AllowAnyMethod()); }); builder.Services.AddControllers().AddNewtonsoftJson(); + + // Read the API key from appsettings.json + var configuration = builder.Configuration; + var apiKey = configuration["ApiSettings:ApiKey"]; + var app = builder.Build(); + + // Middleware to validate API key + // This middleware checks for the presence of the API key in the request headers + // and compares it with the expected API key from appsettings.json. + // If the API key is missing or invalid, it returns a 401 Unauthorized response. + // + /* app.Use(async (context, next) => + { + if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) || extractedApiKey != apiKey) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Unauthorized: Invalid API Key"); + return; + } + + await next(); + }); */ // Apply CORS policy to allow all origins app.UseCors("AllowAllOrigins"); @@ -94,6 +117,88 @@ namespace ResourceMonitorService } }); + /* curl -X POST http://localhost:5000/api/force-shutdown -d "5000" */ + /* Invoke-WebRequest -Uri "http://localhost:5000/api/force-shutdown" -Method POST -Body "50000" -ContentType "text/plain" */ + app.MapPost("/api/force-shutdown", async context => + { + try + { + var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); + var parameters = JsonConvert.DeserializeObject(requestBody); + + string action = parameters?.Action?.ToString()?.ToLower(); // "shutdown" or "restart" + int delaySeconds = parameters?.DelaySeconds ?? 0; + + // Validate action input + if (action != "shutdown" && action != "restart" && action != "cancel") + { + await context.Response.WriteAsync("Invalid action. Use 'shutdown', 'restart', or 'cancel'."); + return; + } + + //if action is stop, then cancel the shutdown + if (action == "cancel") + { + var processStartInfoCancel = new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/a", + CreateNoWindow = true, + UseShellExecute = false + }; + + Process.Start(processStartInfoCancel); + await context.Response.WriteAsync("Shutdown cancelled."); + return; + } + + // Validate delay input + if (delaySeconds < 0) + { + await context.Response.WriteAsync("Delay must be a non-negative integer."); + return; + } + + // Determine the shutdown command + string shutdownCommand = action == "shutdown" ? $"/s /f /t {delaySeconds}" : $"/r /f /t {delaySeconds}"; + + var processStartInfo = new ProcessStartInfo + { + FileName = "shutdown", + Arguments = shutdownCommand, + CreateNoWindow = true, + UseShellExecute = false + }; + + Process.Start(processStartInfo); + + await context.Response.WriteAsync($"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds."); + } + catch (Exception ex) + { + await context.Response.WriteAsync($"An error occurred: {ex.Message}"); + } + }); + + app.MapGet("/api/stop", async context => + { + await context.Response.WriteAsync("Stopping the service..."); + _lifetime.StopApplication(); + }); + + app.MapGet("/", () => "Resource Monitor Service is running."); + app.MapGet("/api/current-time", () => Results.Ok(GetCurrentTime())); + app.MapGet("/api/computer-info", () => Results.Ok(GetComputerInfo())); + app.MapGet("/api/cpu-usage", () => Results.Ok(GetCpuUsage())); + app.MapGet("/api/ram-usage", () => Results.Ok(GetRamUsage())); + app.MapGet("/api/gpu-usage", () => Results.Ok(GetGpuUsage())); + app.MapGet("/api/running-game", () => Results.Ok(GetCurrentlyRunningGame())); + app.MapGet("/api/total-physical-memory", () => Results.Ok(GetTotalPhysicalMemory())); + app.MapGet("/api/total-available-memory", () => Results.Ok(new { TotalAvailableMemory = Environment.WorkingSet })); + + app.MapGet("/health", () => Results.Ok("Service is healthy.")); + + _ = app.RunAsync(stoppingToken); await Task.Delay(Timeout.Infinite, stoppingToken); @@ -126,7 +231,26 @@ namespace ResourceMonitorService if (usage > 80) { // Get the current processes and sort them by CPU usage in descending order - var processes = Process.GetProcesses().OrderByDescending(p => p.TotalProcessorTime); + var processes = Process.GetProcesses() + .Select(p => + { + try + { + return new + { + Process = p, + TotalProcessorTime = p.TotalProcessorTime + }; + } + catch + { + return null; // Skip processes that throw exceptions + } + }) + .Where(p => p != null) + .OrderByDescending(p => p.TotalProcessorTime) + .Select(p => p.Process) + .ToList(); // Create a new anonymous type containing the CPU usage, RAM usage, and the top 3 highest CPU-using processes return new @@ -190,28 +314,87 @@ namespace ResourceMonitorService 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 + /* if (!IsNvidiaGpuPresent()) { - Usage = utilization.Gpu, - Temperature = temperature, - FanSpeed = fanSpeed - }; + return new + { + Usage = 0, + Temperature = 0, + FanSpeed = 0, + IsAvailable = false, + Message = "No NVIDIA GPU detected" + }; + } */ + + try + { + + NvmlWrapper.NvmlInit(); + IntPtr device; + NvmlWrapper.NvmlDeviceGetHandleByIndex(0, out device); + NvmlWrapper.NvmlUtilization utilization; + NvmlWrapper.NvmlDeviceGetUtilizationRates(device, out utilization); + + uint temperature; + NvmlWrapper.NvmlDeviceGetTemperature(device, 0, out temperature); + + uint fanSpeed; + NvmlWrapper.NvmlDeviceGetFanSpeed(device, out fanSpeed); + + NvmlWrapper.NvmlShutdown(); + + return new + { + Usage = utilization.Gpu, + Temperature = temperature, + FanSpeed = fanSpeed, + IsAvailable = false, + Error = "" + }; + } + catch (Exception ex) + { + return new + { + Usage = 0, + Temperature = 0, + FanSpeed = 0, + IsAvailable = false, + Error = ex.Message + }; + } } + /* private bool IsNvidiaGpuPresent() + { + try + { + // Method 1: Try to initialize NVML + NvmlWrapper.NvmlInit(); + uint deviceCount = 0; + NvmlWrapper.NvmlDeviceGetCount(ref deviceCount); + NvmlWrapper.NvmlShutdown(); + + return deviceCount > 0; + } + catch + { + // Method 2: Fallback to checking using WMI + try + { + using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController WHERE Name LIKE '%NVIDIA%'")) + { + var collection = searcher.Get(); + return collection.Count > 0; + } + } + catch + { + return false; + } + } + } */ + private object GetCurrentlyRunningGame() { var processes = Process.GetProcesses(); diff --git a/appsettings.json b/appsettings.json index 7d910a4..78a1652 100644 --- a/appsettings.json +++ b/appsettings.json @@ -12,5 +12,8 @@ "Url": "http://*:5000" } } + }, + "ApiSettings": { + "ApiKey": "b7f3e8a1-4c2d-4d9f-9a6e-2a1c5d7f8e9a" } } diff --git a/publish/configure_firewall.ps1 b/publish/configure_firewall.ps1 index b2ee377..143c5cd 100644 --- a/publish/configure_firewall.ps1 +++ b/publish/configure_firewall.ps1 @@ -1,6 +1,6 @@ # PowerShell script to create a new inbound rule in Windows Firewall $port = 5000 -$ruleName = "ResourceMonitorService" +$ruleName = "ResourceMonitorServicePublish" if (Get-NetFirewallRule -DisplayName $ruleName) { Write-Host "Rule already exists, not creating a new one" } else { diff --git a/publish/install-service.bat b/publish/install-service.bat index 5728052..808fc57 100644 --- a/publish/install-service.bat +++ b/publish/install-service.bat @@ -1,4 +1,4 @@ -sc create ResourceMonitorServicerrr binPath= "%~dp0ResourceMonitorService.exe --windows-service" start= auto -sc description ResourceMonitorServicerrr "A service that monitors system resource usage and exposes it via a web API." +sc create ResourceMonitorService binPath="%~dp0ResourceMonitorService.exe --windows-service" start= auto +sc description ResourceMonitorService "A service that monitors system resource usage and exposes it via a web API." diff --git a/publish/publish/appsettings.Development.json b/publish/publish/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/publish/publish/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/publish/publish/appsettings.json b/publish/publish/appsettings.json new file mode 100644 index 0000000..78a1652 --- /dev/null +++ b/publish/publish/appsettings.json @@ -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" + } +} diff --git a/publish/publish/publish/appsettings.Development.json b/publish/publish/publish/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/publish/publish/publish/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/publish/publish/publish/appsettings.json b/publish/publish/publish/appsettings.json new file mode 100644 index 0000000..78a1652 --- /dev/null +++ b/publish/publish/publish/appsettings.json @@ -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" + } +} diff --git a/publish/publish/publish/publish/appsettings.Development.json b/publish/publish/publish/publish/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/publish/publish/publish/publish/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/publish/publish/publish/publish/appsettings.json b/publish/publish/publish/publish/appsettings.json new file mode 100644 index 0000000..78a1652 --- /dev/null +++ b/publish/publish/publish/publish/appsettings.json @@ -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" + } +}