315 lines
11 KiB
C#
315 lines
11 KiB
C#
// 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<ProcessMonitor> _logger;
|
|
private readonly MonitoringSettings _config;
|
|
|
|
private readonly object _lock = new object();
|
|
private ConcurrentDictionary<int, ProcessInfo> _processesBuffer = new();
|
|
|
|
private DateTime ScanTime
|
|
{
|
|
set
|
|
{
|
|
_lastScanTime = _currentScanTime;
|
|
_currentScanTime = value;
|
|
}
|
|
}
|
|
|
|
private TimeSpan Elapsed
|
|
{
|
|
get => _currentScanTime - _lastScanTime;
|
|
}
|
|
|
|
public IEnumerable<ProcessBaseInfo> 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<ProcessMonitor> logger, IOptions<AppSettings> 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<MemoryRegionInfo> 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<MemoryRegionInfo>();
|
|
|
|
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<int, (string? Name, string? CommandLine, int ParentPID)> 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<int, ProcessInfo>();
|
|
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<int, ProcessInfo>(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<int, (string? Name, string? CommandLine, int ParentPID)> GetWmiProcessData()
|
|
{
|
|
var data = new Dictionary<int, (string?, string?, int)>();
|
|
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();
|
|
}
|
|
}
|
|
}
|