// File: Services/ProcessMonitor.cs using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Management; using System.Runtime.Versioning; using WebmrAPI.Configuration; using WebmrAPI.Models; namespace WebmrAPI.Services { [SupportedOSPlatform("windows")] public class ProcessMonitor : IHostedService, IDisposable { private DateTime _lastScanTime = DateTime.MinValue; private DateTime _currentScanTime = DateTime.MinValue; private Timer? _timer; private readonly ILogger _logger; private readonly MonitoringSettings _config; private readonly object _lock = new object(); private ConcurrentDictionary _processesBuffer = new(); private DateTime ScanTime { set { _lastScanTime = _currentScanTime; _currentScanTime = value; } } private TimeSpan Elapsed { get => _currentScanTime - _lastScanTime; } public IEnumerable GetBufferedProcesses() { lock (_lock) { return _processesBuffer.Values.ToList(); } } public ProcessInfo? GetProcessDetails(int pid) { lock (_lock) { if (_processesBuffer.TryGetValue(pid, out ProcessInfo? data)) { return PopulateProcessInfo(data); } return null; } } public ProcessMonitor(ILogger logger, IOptions settings) { _logger = logger; _config = settings.Value.Monitoring; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation($"ProcessMonitor started. Scan interval: {_config.ProcessScanInterval} seconds."); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_config.ProcessScanInterval)); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("ProcessMonitor is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } private bool OpenProcess(ProcessInfo process, out IntPtr ctx) { ctx = WinApi.OpenProcess(WinApi.PROCESS_QUERY_INFORMATION | WinApi.PROCESS_VM_OPERATION, false, (uint)process.PID); if (ctx == IntPtr.Zero) { var errno = WinApi.LastError; if (errno == 5) { _logger.LogWarning($"Access denied to process {process.Name} (PID: {process.PID})"); } else { _logger.LogError($"WinAPI call 'OpenProcess for PID {process.PID}' failed", errno); } return false; } return true; } private ProcessInfo PopulateProcessInfo(ProcessInfo dst, List regions) { dst.MemoryRegions = regions; dst.LastUpdate = DateTime.UtcNow; return dst; } private ProcessInfo PopulateProcessInfo(ProcessInfo dst) { if ((DateTime.UtcNow - dst.LastUpdate).TotalSeconds < _config.MemoryRegionScanTimeout) return dst; IntPtr cur = IntPtr.Zero; IntPtr ctx = IntPtr.Zero; long next = 0; var regions = new List(); try { if (OpenProcess(dst, out ctx)) while ((next > cur.ToInt64() && next < 0x7fffffffffff) || next == 0) { WinApi.MEMORY_BASIC_INFORMATION mbi; cur = new IntPtr(next); if (WinApi.VirtualQueryEx(ctx, cur, out mbi, WinApi.MBISize) == 0) { _logger.LogWarning($"VirtualQueryEx for PID {dst.PID} at address 0x{cur.ToInt64():X12} failed"); break; } if (mbi.State == WinApi.MemoryState.Commit || mbi.State == WinApi.MemoryState.Reserve) { regions.Add(new MemoryRegionInfo { MemoryAddress = mbi.BaseAddress.ToInt64(), MemorySize = mbi.RegionSize.ToUInt64(), MemoryState = mbi.State, MemoryPageProtection = mbi.PageProtection, MemoryType = mbi.Type }); } next = mbi.BaseAddress.ToInt64() + (long)mbi.RegionSize.ToUInt64(); } } finally { if (ctx != IntPtr.Zero) WinApi.CloseHandle(ctx); } return PopulateProcessInfo(dst, regions); } private void PopulateBaseProcessInfo(ProcessInfo dst, System.Diagnostics.Process process, Dictionary wmiData) { dst.PID = process.Id; dst.MemorySize = (ulong)process.WorkingSet64; dst.ThreadCount = process.Threads.Count; dst.TotalProcessorTime = process.TotalProcessorTime; dst.MemoryAddress = 0x000000000000; if (wmiData.TryGetValue(process.Id, out var entry)) { dst.Name = entry.Name; dst.CommandLine = entry.CommandLine; dst.ParentPID = entry.ParentPID; } else { dst.Name = process.ProcessName; _logger.LogDebug("No WMI data was found for PID {pid}.", process.Id); } try { if (process.MainModule != null) { dst.FileName = process.MainModule.FileName; dst.MemoryAddress = process.MainModule.BaseAddress.ToInt64(); } else { _logger.LogDebug($"Process {process.ProcessName} (PID: {process.Id}) process has no MainModule"); } } catch (System.ComponentModel.Win32Exception ex) { _logger.LogDebug($"Process {process.ProcessName} (PID: {process.Id}), getting process address ends with message: {ex.Message}"); } catch (Exception ex) { _logger.LogError($"Process {process.ProcessName} (PID: {process.Id}), an error occurred while getting process address: {ex.Message}"); } try { dst.StartTime = process.StartTime; } catch (Exception ex) { _logger.LogDebug("Could not get StartTime for PID {Pid}: {Message}", process.Id, ex.Message); } try { process.Refresh(); dst.Status = process.Responding ? ProcessStatus.Running : ProcessStatus.NotResponding; } catch (Exception ex) { dst.Status = ProcessStatus.Undefined; _logger.LogDebug("Could not get Status for PID {Pid}: {Message}", process.Id, ex.Message); } } private void DoWork(object? state) { _logger.LogDebug("Initiating process scan..."); try { var cur = new Dictionary(); var wmiData = GetWmiProcessData(); ScanTime = DateTime.UtcNow; foreach (var process in System.Diagnostics.Process.GetProcesses()) { ProcessInfo? processInfo; if (process.Id == 0 || process.Id == 4) { process.Dispose(); continue; } if (_processesBuffer.TryGetValue(process.Id, out var existingProcessInfo)) { processInfo = existingProcessInfo; } else { processInfo = new ProcessInfo(); } try { PopulateBaseProcessInfo(processInfo, process, wmiData); if (_lastScanTime != DateTime.MinValue && Elapsed.TotalMilliseconds > 0) { processInfo.CpuUsage = (processInfo.ProcessorTime / Elapsed.TotalMilliseconds) / Environment.ProcessorCount * 100.0; if (processInfo.CpuUsage > 100.0) processInfo.CpuUsage = 100.0; } else { processInfo.CpuUsage = 0.0; } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to get full information for process {ProcessId} ({ProcessName}).", process.Id, process.ProcessName); } finally { cur.TryAdd(process.Id, processInfo); process?.Dispose(); } } lock (_lock) { _processesBuffer = new ConcurrentDictionary(cur); } _logger.LogInformation("Process buffer updated, contains {Count} processes.", _processesBuffer.Count); } catch (Exception ex) { _logger.LogError(ex, "Unhandled error during process monitoring cycle."); } } private Dictionary GetWmiProcessData() { var data = new Dictionary(); try { using (var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine, ParentProcessId FROM Win32_Process")) using (ManagementObjectCollection processes = searcher.Get()) { foreach (ManagementObject obj in processes) { int pid = Convert.ToInt32(obj["ProcessId"]); int ppid = Convert.ToInt32(obj["ParentProcessId"]); string? name = obj["Name"]?.ToString(); string? cmd = obj["CommandLine"]?.ToString(); data[pid] = (name, cmd, ppid); } } } catch (Exception ex) { _logger.LogError(ex, "The data of the WMI process could not be retrieved."); } return data; } public void Dispose() { _timer?.Dispose(); } } }