3d47fc1439
- Created ResourceHub.cs for SignalR group management. - Developed a modern web dashboard using Tailwind CSS for responsive design. - Implemented real-time updates with SignalR for CPU, Memory, GPU, and Network usage. - Added REST API endpoints for resource information and process management. - Integrated process management features to view and terminate high-usage processes. - Enhanced UI with loading spinners, notifications, and responsive tables. - Included performance charts for historical CPU and Memory usage. - Configured Swagger UI for API documentation. - Established security features including process kill restrictions and API key authentication.
556 lines
23 KiB
C#
556 lines
23 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ResourceMonitorService.Configuration;
|
|
using ResourceMonitorService.Models;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace ResourceMonitorService.Services
|
|
{
|
|
public interface IGameDetectionService
|
|
{
|
|
Task<GameInfo?> GetCurrentlyRunningGameAsync();
|
|
Task<List<GameInfo>> GetAllDetectedGamesAsync();
|
|
Task<bool> IsGameRunningFullscreenAsync();
|
|
Task<float> GetGameFpsAsync(string processName);
|
|
}
|
|
|
|
public class GameDetectionService : IGameDetectionService
|
|
{
|
|
private readonly ILogger<GameDetectionService> _logger;
|
|
private readonly MonitoringSettings _settings;
|
|
|
|
// Windows API imports for fullscreen detection
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr GetForegroundWindow();
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern int GetWindowTextLength(IntPtr hWnd);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool IsWindowVisible(IntPtr hWnd);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct RECT
|
|
{
|
|
public int Left;
|
|
public int Top;
|
|
public int Right;
|
|
public int Bottom;
|
|
}
|
|
|
|
public GameDetectionService(ILogger<GameDetectionService> logger, IOptions<MonitoringSettings> settings)
|
|
{
|
|
_logger = logger;
|
|
_settings = settings.Value;
|
|
}
|
|
|
|
public async Task<GameInfo?> GetCurrentlyRunningGameAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var processes = Process.GetProcesses();
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
try
|
|
{
|
|
if (process.MainModule?.FileName == null)
|
|
continue;
|
|
|
|
var filePath = process.MainModule.FileName;
|
|
var gameInfo = DetectGameFromPath(filePath, process);
|
|
|
|
if (gameInfo != null)
|
|
{
|
|
gameInfo.IsFullscreen = IsGameRunningFullscreenAsync().Result;
|
|
gameInfo.FPS = GetGameFpsAsync(process.ProcessName).Result;
|
|
return gameInfo;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Handle access exceptions silently - some processes can't be accessed
|
|
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error detecting currently running game");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<List<GameInfo>> GetAllDetectedGamesAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var games = new List<GameInfo>();
|
|
var processes = Process.GetProcesses();
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
try
|
|
{
|
|
if (process.MainModule?.FileName == null)
|
|
continue;
|
|
|
|
var filePath = process.MainModule.FileName;
|
|
var gameInfo = DetectGameFromPath(filePath, process);
|
|
|
|
if (gameInfo != null)
|
|
{
|
|
games.Add(gameInfo);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogTrace(ex, "Could not access process {ProcessName}", process.ProcessName);
|
|
}
|
|
}
|
|
|
|
return games;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting all detected games");
|
|
return new List<GameInfo>();
|
|
}
|
|
}
|
|
|
|
public async Task<bool> IsGameRunningFullscreenAsync()
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
var foregroundWindow = GetForegroundWindow();
|
|
if (foregroundWindow == IntPtr.Zero)
|
|
return false;
|
|
|
|
if (!IsWindowVisible(foregroundWindow))
|
|
return false;
|
|
|
|
if (!GetWindowRect(foregroundWindow, out RECT rect))
|
|
return false;
|
|
|
|
// Get screen dimensions
|
|
var primaryScreen = System.Windows.Forms.Screen.PrimaryScreen;
|
|
if (primaryScreen == null)
|
|
return false;
|
|
var screenWidth = primaryScreen.Bounds.Width;
|
|
var screenHeight = primaryScreen.Bounds.Height;
|
|
|
|
// Check if window covers the entire screen
|
|
var windowWidth = rect.Right - rect.Left;
|
|
var windowHeight = rect.Bottom - rect.Top;
|
|
|
|
return windowWidth >= screenWidth && windowHeight >= screenHeight;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not determine if game is running fullscreen");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<float> GetGameFpsAsync(string processName)
|
|
{
|
|
try
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
// This is a simplified FPS detection - in reality, you'd need more sophisticated methods
|
|
// such as hooking into DirectX/OpenGL or using external tools like RTSS
|
|
|
|
// For now, we'll return 0 as a placeholder
|
|
// In a real implementation, you might:
|
|
// 1. Use Windows Performance Toolkit (WPT) ETW events
|
|
// 2. Hook into D3D11/D3D12 present calls
|
|
// 3. Use NVIDIA's NVAPI or AMD's ADL
|
|
// 4. Parse log files from games that output FPS
|
|
|
|
return 0f;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not get FPS for process {ProcessName}", processName);
|
|
return 0f;
|
|
}
|
|
}
|
|
|
|
private GameInfo? DetectGameFromPath(string filePath, Process process)
|
|
{
|
|
try
|
|
{
|
|
// First check configured game root folders
|
|
var gameFromRootFolder = DetectGameFromRootFolders(filePath, process);
|
|
if (gameFromRootFolder != null)
|
|
{
|
|
return gameFromRootFolder;
|
|
}
|
|
|
|
// Check each configured game platform path
|
|
foreach (var platformPath in _settings.GamePlatformPaths)
|
|
{
|
|
if (filePath.Contains(platformPath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var platform = GetPlatformFromPath(platformPath);
|
|
var gameName = ExtractGameNameFromPath(filePath, platformPath);
|
|
|
|
return new GameInfo
|
|
{
|
|
GameName = gameName,
|
|
ExecutableName = Path.GetFileName(filePath),
|
|
FullPath = filePath,
|
|
ProcessId = process.Id,
|
|
MemoryUsage = (ulong)process.WorkingSet64,
|
|
CpuTime = process.TotalProcessorTime,
|
|
StartTime = process.StartTime,
|
|
Platform = platform,
|
|
IsFullscreen = false, // Will be set by caller
|
|
FPS = 0f // Will be set by caller
|
|
};
|
|
}
|
|
}
|
|
|
|
// Additional checks for common game launchers and executables
|
|
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
|
|
|
|
// Exclude known system processes and applications
|
|
var systemExclusions = new[]
|
|
{
|
|
"officeclicktorun", "winword", "excel", "powerpoint", "outlook",
|
|
"teams", "skype", "chrome", "firefox", "edge", "explorer",
|
|
"notepad", "calculator", "cmd", "powershell", "taskmgr",
|
|
"svchost", "dwm", "csrss", "winlogon", "lsass", "services",
|
|
"wininit", "audiodg", "conhost", "rundll32", "msiexec",
|
|
"setup", "installer", "update", "vshost", "devenv"
|
|
};
|
|
|
|
// Exclude gaming platform clients (they are not games themselves)
|
|
var platformClientExclusions = new[]
|
|
{
|
|
"steam", "steamwebhelper", "steamservice", "steamerrorreporter",
|
|
"epicgameslauncher", "epic games launcher", "epiconlineservices",
|
|
"origin", "originwebhelperservice", "originweb", "originthinsetup",
|
|
"uplay", "upc", "ubisoftgamelauncher", "ubisoftconnect",
|
|
"battle.net", "battlenet", "blizzardlauncher", "blizzard update agent",
|
|
"gog galaxy", "goggalaxy", "gogcom",
|
|
"rockstar games launcher", "rockstargameslauncher",
|
|
"riotclientservices", "riot client", "valorant-win64-shipping",
|
|
"ea app", "ea desktop", "eadesktop", "eabackground",
|
|
"xbox", "xboxapp", "xboxgamebar", "microsoftstore"
|
|
};
|
|
|
|
// Skip if it's a known system process or platform client
|
|
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)) ||
|
|
platformClientExclusions.Any(exclusion => fileName.Contains(exclusion)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var knownGameExecutables = new[]
|
|
{
|
|
"game"
|
|
// Removed "launcher" and "client" as they often refer to platform clients, not games
|
|
};
|
|
|
|
var gameIndicators = new[]
|
|
{
|
|
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
|
|
"\\steamapps\\common\\", "\\epic games\\", "\\gog galaxy\\games\\",
|
|
"\\origin games\\", "\\ubisoft game launcher\\games\\"
|
|
};
|
|
|
|
// Check if it's likely a game based on executable name or path
|
|
// Made the condition more restrictive to reduce false positives
|
|
if ((knownGameExecutables.Any(exe => fileName.Equals(exe) || fileName.StartsWith(exe + ".")) ||
|
|
gameIndicators.Any(indicator => filePath.Contains(indicator, StringComparison.OrdinalIgnoreCase))) &&
|
|
!filePath.Contains("Program Files\\Common Files", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Try to determine platform and game name from other indicators
|
|
var platform = DeterminePlatformFromProcess(process, filePath);
|
|
var gameName = DetermineGameNameFromProcess(process, filePath);
|
|
|
|
if (!string.IsNullOrEmpty(gameName))
|
|
{
|
|
return new GameInfo
|
|
{
|
|
GameName = gameName,
|
|
ExecutableName = Path.GetFileName(filePath),
|
|
FullPath = filePath,
|
|
ProcessId = process.Id,
|
|
MemoryUsage = (ulong)process.WorkingSet64,
|
|
CpuTime = process.TotalProcessorTime,
|
|
StartTime = process.StartTime,
|
|
Platform = platform,
|
|
IsFullscreen = false,
|
|
FPS = 0f
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error detecting game from path {FilePath}", filePath);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string GetPlatformFromPath(string platformPath)
|
|
{
|
|
return platformPath.ToLowerInvariant() switch
|
|
{
|
|
var path when path.Contains("steamapps") => "Steam",
|
|
var path when path.Contains("epic games") => "Epic Games Store",
|
|
var path when path.Contains("gog galaxy") => "GOG Galaxy",
|
|
var path when path.Contains("origin games") => "EA Origin",
|
|
var path when path.Contains("ubisoft game launcher") => "Ubisoft Connect",
|
|
_ => "Unknown"
|
|
};
|
|
}
|
|
|
|
private string ExtractGameNameFromPath(string filePath, string platformPath)
|
|
{
|
|
try
|
|
{
|
|
var parts = filePath.Split(new[] { platformPath }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length > 1)
|
|
{
|
|
var gamePath = parts[1];
|
|
var gameFolder = gamePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[0];
|
|
return gameFolder;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not extract game name from path {FilePath}", filePath);
|
|
}
|
|
|
|
return Path.GetFileNameWithoutExtension(filePath);
|
|
}
|
|
|
|
private string DeterminePlatformFromProcess(Process process, string filePath)
|
|
{
|
|
try
|
|
{
|
|
// Check parent processes for launcher indicators
|
|
var currentProcess = process;
|
|
for (int i = 0; i < 3; i++) // Check up to 3 levels up
|
|
{
|
|
try
|
|
{
|
|
var parentId = GetParentProcessId(currentProcess.Id);
|
|
if (parentId == 0) break;
|
|
|
|
var parentProcess = Process.GetProcessById(parentId);
|
|
var parentName = parentProcess.ProcessName.ToLowerInvariant();
|
|
|
|
if (parentName.Contains("steam"))
|
|
return "Steam";
|
|
if (parentName.Contains("epic"))
|
|
return "Epic Games Store";
|
|
if (parentName.Contains("origin"))
|
|
return "EA Origin";
|
|
if (parentName.Contains("uplay") || parentName.Contains("ubisoft"))
|
|
return "Ubisoft Connect";
|
|
if (parentName.Contains("gog"))
|
|
return "GOG Galaxy";
|
|
|
|
currentProcess = parentProcess;
|
|
}
|
|
catch
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback: check file path for platform indicators
|
|
if (filePath.Contains("Program Files (x86)"))
|
|
return "Windows Store/Other";
|
|
if (filePath.Contains("WindowsApps"))
|
|
return "Microsoft Store";
|
|
|
|
return "Standalone";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not determine platform for process {ProcessName}", process.ProcessName);
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
private string DetermineGameNameFromProcess(Process process, string filePath)
|
|
{
|
|
try
|
|
{
|
|
// Try to get a meaningful name from various sources
|
|
|
|
// 1. Try from file properties
|
|
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
|
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
|
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Check if this is a platform client by product name
|
|
var platformClientNames = new[]
|
|
{
|
|
"steam", "epic games launcher", "origin", "uplay", "ubisoft connect",
|
|
"battle.net", "blizzard launcher", "gog galaxy", "riot client",
|
|
"ea app", "ea desktop", "xbox", "microsoft store", "rockstar games launcher"
|
|
};
|
|
|
|
if (platformClientNames.Any(client =>
|
|
versionInfo.ProductName.Contains(client, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return string.Empty; // Return empty to indicate this should not be treated as a game
|
|
}
|
|
|
|
return versionInfo.ProductName;
|
|
}
|
|
|
|
// 2. Try from directory name
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
if (!string.IsNullOrEmpty(directory))
|
|
{
|
|
var directoryName = Path.GetFileName(directory);
|
|
if (!string.IsNullOrEmpty(directoryName) &&
|
|
!directoryName.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
|
|
!directoryName.Equals("exe", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return directoryName;
|
|
}
|
|
}
|
|
|
|
// 3. Fallback to executable name
|
|
return Path.GetFileNameWithoutExtension(filePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not determine game name for process {ProcessName}", process.ProcessName);
|
|
return process.ProcessName;
|
|
}
|
|
}
|
|
|
|
private GameInfo? DetectGameFromRootFolders(string filePath, Process process)
|
|
{
|
|
try
|
|
{
|
|
foreach (var rootFolder in _settings.GameRootFolders)
|
|
{
|
|
if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var gameName = ExtractGameNameFromRootFolder(filePath, rootFolder);
|
|
|
|
return new GameInfo
|
|
{
|
|
GameName = gameName,
|
|
ExecutableName = Path.GetFileName(filePath),
|
|
FullPath = filePath,
|
|
ProcessId = process.Id,
|
|
MemoryUsage = (ulong)process.WorkingSet64,
|
|
CpuTime = process.TotalProcessorTime,
|
|
StartTime = process.StartTime,
|
|
Platform = "Standalone", // Games in root folders are typically standalone
|
|
IsFullscreen = false, // Will be set by caller
|
|
FPS = 0f // Will be set by caller
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error detecting game from root folders for path {FilePath}", filePath);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string ExtractGameNameFromRootFolder(string filePath, string rootFolder)
|
|
{
|
|
try
|
|
{
|
|
// Remove the root folder from the path to get the relative game path
|
|
var relativePath = filePath.Substring(rootFolder.Length).TrimStart('\\', '/');
|
|
|
|
// Split by directory separator and take the first part as the game folder
|
|
var pathParts = relativePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
|
|
StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (pathParts.Length > 0)
|
|
{
|
|
var gameFolder = pathParts[0];
|
|
|
|
// If the game folder name is reasonable, use it
|
|
if (!string.IsNullOrEmpty(gameFolder) &&
|
|
!gameFolder.Equals("bin", StringComparison.OrdinalIgnoreCase) &&
|
|
!gameFolder.Equals("exe", StringComparison.OrdinalIgnoreCase) &&
|
|
!gameFolder.Equals("data", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return gameFolder;
|
|
}
|
|
}
|
|
|
|
// Fallback: try to get the game name from file properties
|
|
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
|
|
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
|
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return versionInfo.ProductName;
|
|
}
|
|
|
|
// Last resort: use the executable name
|
|
return Path.GetFileNameWithoutExtension(filePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not extract game name from root folder path {FilePath}", filePath);
|
|
return Path.GetFileNameWithoutExtension(filePath);
|
|
}
|
|
}
|
|
|
|
private int GetParentProcessId(int processId)
|
|
{
|
|
try
|
|
{
|
|
using var searcher = new System.Management.ManagementObjectSearcher(
|
|
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {processId}");
|
|
using var collection = searcher.Get();
|
|
foreach (System.Management.ManagementObject obj in collection)
|
|
{
|
|
return Convert.ToInt32(obj["ParentProcessId"]);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogTrace(ex, "Could not get parent process ID for {ProcessId}", processId);
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
}
|