This commit is contained in:
Gregory Lirent 2025-07-03 15:22:40 +03:00
parent 09af9d5091
commit 802598be79
28 changed files with 1102 additions and 531 deletions

View File

@ -12,6 +12,7 @@ namespace WebmrAPI.Configuration
{
public int ProcessScanInterval { get; set; } = 5;
public int MemoryRegionScanTimeout { get; set; } = 30;
public bool AllowProcessReadAccess { get; set; } = true;
}
public class WebServerSettings

View File

@ -0,0 +1,9 @@
// File: Exceptions/GettingModuleInfoException.cs
namespace WebmrAPI.Exceptions
{
public class GettingModuleInfoException : ProcessMonitorException
{
public GettingModuleInfoException(string message) : base(message) { }
}
}

View File

@ -0,0 +1,9 @@
// File: Exceptions/MemoryRegionException.cs
namespace WebmrAPI.Exceptions
{
public class MemoryRegionException : ProcessMonitorException
{
public MemoryRegionException(string message) : base(message) { }
}
}

View File

@ -0,0 +1,9 @@
// File: Exceptions/ProcessAccessDeniedException.cs
namespace WebmrAPI.Exceptions
{
public class ProcessAccessDeniedException : ProcessMonitorException
{
public ProcessAccessDeniedException(string message) : base(message) { }
}
}

View File

@ -1,4 +1,6 @@
namespace WebmrAPI.Exceptions
// File: Exceptions/ProcessMonitorException.cs
namespace WebmrAPI.Exceptions
{
public class ProcessMonitorException : Exception
{

View File

@ -1,10 +1,11 @@
// File: Models/MemoryRegion.cs
using System.Text.Json.Serialization;
using WebmrAPI.Utils;
namespace WebmrAPI.Models
{
public class MemoryRegion : ConcurrentDTO
public class MemoryRegion : ConcurrentObject
{
private long _addr = 0;
private ulong _size = 0;

View File

@ -1,26 +1,26 @@
// File: Models/MemoryRegionInfo.cs
using WebmrAPI.Services;
using WebmrAPI.Utils;
namespace WebmrAPI.Models
{
public class MemoryRegionInfo : MemoryRegion
{
private WinApi.MemoryState _state = 0;
private WinApi.MemoryPageProtectionState _protect = 0;
private WinApi.MemoryType _type = 0;
private WindowsProcess.MemoryState _state = 0;
private WindowsProcess.MemoryPageProtectionState _protect = 0;
private WindowsProcess.MemoryType _type = 0;
public WinApi.MemoryState MemoryState
public WindowsProcess.MemoryState MemoryState
{
get => LockedGet(ref _state);
set => LockedSet(ref _state, value);
}
public WinApi.MemoryPageProtectionState MemoryPageProtection
public WindowsProcess.MemoryPageProtectionState MemoryPageProtection
{
get => LockedGet(ref _protect);
set => LockedSet(ref _protect, value);
}
public WinApi.MemoryType MemoryType
public WindowsProcess.MemoryType MemoryType
{
get => LockedGet(ref _type);
set => LockedSet(ref _type, value);

View File

@ -4,15 +4,15 @@ using System.Text.Json.Serialization;
namespace WebmrAPI.Models
{
public enum ProcessStatus
{
Undefined,
Running,
NotResponding
}
public class ProcessBaseInfo : MemoryRegion
{
public enum ProcessStatus
{
Undefined,
Running,
NotResponding
}
private TimeSpan _lastPTime = TimeSpan.Zero;
private TimeSpan _curPTime = TimeSpan.Zero;

View File

@ -2,25 +2,36 @@
using System.Runtime.Versioning;
using System.Text.Json.Serialization;
using WebmrAPI.Utils;
namespace WebmrAPI.Models
{
[SupportedOSPlatform("windows")]
public class ProcessInfo : ProcessBaseInfo
{
private DateTime _lastUpdate = DateTime.MinValue;
private List<MemoryRegionInfo> _regions = new();
[JsonIgnore]
public DateTime LastUpdate
public LazyConcurrentContainer<MemoryRegionInfo> MemoryRegionsContainer { get; set; } = new();
[JsonIgnore]
public LazyConcurrentContainer<ProcessModuleInfo> ModulesContainer { get; set; } = new();
[JsonIgnore]
public LazyConcurrentContainer<ProcessThreadInfo> ThreadsContainer { get; set; } = new();
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<MemoryRegionInfo>? MemoryRegions
{
get => LockedGet(ref _lastUpdate);
set => LockedSet(ref _lastUpdate, value);
get => MemoryRegionsContainer.Values;
}
public List<MemoryRegionInfo> MemoryRegions
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<ProcessModuleInfo>? Modules
{
get => LockedGet(ref _regions);
set => LockedSet(ref _regions, value);
get => ModulesContainer.Values;
}
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<ProcessThreadInfo>? Threads
{
get => ThreadsContainer.Values;
}
}
}

View File

@ -0,0 +1,37 @@
// File: Models/ProcessModuleInfo.cs
using System.Text.Json.Serialization;
namespace WebmrAPI.Models
{
public class ProcessModuleInfo : MemoryRegion
{
private string? _moduleName = null;
private string? _fileName = null;
private long _entrypointAddress = 0;
public string? ModuleName
{
get => LockedGet(ref _moduleName);
set => LockedSet(ref _moduleName, value);
}
public string? FileName
{
get => LockedGet(ref _fileName);
set => LockedSet(ref _fileName, value);
}
public string? EntrypointAddress
{
get => (EntrypointRawAddress > 0) ? $"0x{EntrypointRawAddress:X16}" : null;
}
[JsonIgnore]
public long EntrypointRawAddress
{
get => LockedGet(ref _entrypointAddress);
set => LockedSet(ref _entrypointAddress, value);
}
}
}

View File

@ -0,0 +1,52 @@
// File: Models/ProcessThreadInfo.cs
using System.Text.Json.Serialization;
using WebmrAPI.Utils;
namespace WebmrAPI.Models
{
public class ProcessThreadInfo : ConcurrentObject
{
private TimeSpan _lastPTime = TimeSpan.Zero;
private TimeSpan _curPTime = TimeSpan.Zero;
private int _id = 0;
private int _currentPriority = 0;
private int _basePriority = 0;
private double _cpuUsage = 0;
[JsonIgnore]
public TimeSpan TotalProcessorTime
{
set { lock (_lock) { _lastPTime = _curPTime; _curPTime = value; } }
}
[JsonIgnore]
public double ProcessorTime
{
get { lock (_lock) return (_curPTime - _lastPTime).TotalMilliseconds; }
}
public int ID
{
get => LockedGet(ref _id);
set => LockedSet(ref _id, value);
}
public int CurrentPriority
{
get => LockedGet(ref _currentPriority);
set => LockedSet(ref _currentPriority, value);
}
public int BasePriority
{
get => LockedGet(ref _basePriority);
set => LockedSet(ref _basePriority, value);
}
public double CpuUsage
{
get => LockedGet(ref _cpuUsage);
set => LockedSet(ref _cpuUsage, value);
}
}
}

View File

@ -1,65 +1,62 @@
// File: Services/ProcessMonitor.cs
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Management;
using System.Runtime.Versioning;
using WebmrAPI.Configuration;
using WebmrAPI.Exceptions;
using WebmrAPI.Models;
using WebmrAPI.Services.Scanners;
using WebmrAPI.Utils;
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 MonitoringSettings _config;
private LazyConcurrentContainer<ProcessInfo> _processes = new();
private ScanProvider _provider;
private readonly object _lock = new object();
private ConcurrentDictionary<int, ProcessInfo> _processesBuffer = new();
public ILogger<ProcessMonitor> Logger { get => _logger; }
public MonitoringSettings Config { get => _config; }
public LazyConcurrentContainer<ProcessInfo> Processes { get => _processes; }
private DateTime ScanTime
public IEnumerable<ProcessBaseInfo>? GetBufferedProcesses()
{
set
{
_lastScanTime = _currentScanTime;
_currentScanTime = value;
}
return _processes.Values;
}
private TimeSpan Elapsed
public ProcessInfo? GetProcessDetails(int pid, ScanTarget target = ScanTarget.ProcessDetails)
{
get => _currentScanTime - _lastScanTime;
}
public IEnumerable<ProcessBaseInfo> GetBufferedProcesses()
{
lock (_lock)
if (_processes.Container != null && _processes.Container.TryGetValue(pid, out var info))
{
return _processesBuffer.Values.ToList();
}
}
public ProcessInfo? GetProcessDetails(int pid)
{
lock (_lock)
{
if (_processesBuffer.TryGetValue(pid, out ProcessInfo? data))
try
{
return PopulateProcessInfo(data);
_provider.CreateScanTask(info, target).Scan();
_logger.LogInformation($"Scan details of the process {info.Name} (PID: {info.PID}) was completed.");
return info;
}
catch (ProcessMonitorException ex)
{
_logger.LogWarning(ex.Message);
}
catch (Exception ex)
{
_logger.LogError($"Unhandled error during scanning of the process {info.Name} (PID: {info.PID}): {ex.Message}");
}
return null;
}
return null;
}
public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> settings)
{
_logger = logger;
_config = settings.Value.Monitoring;
_provider = new(this);
}
public Task StartAsync(CancellationToken cancellationToken)
@ -76,236 +73,24 @@ namespace WebmrAPI.Services
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);
_provider.CreateScanTask().Scan();
_logger.LogInformation($"Process buffer updated, contains {Processes.Container?.Count} processes.");
}
catch (ProcessMonitorException ex)
{
_logger.LogWarning(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error during process monitoring cycle.");
_logger.LogError($"Unhandled error during process monitoring cycle: {ex.Message}");
}
}
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();

View File

@ -0,0 +1,23 @@
// File: Services/Scanners/AbstractCpuScanner.cs
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public abstract class AbstractCpuScanner<T> : AbstractScanner<T> where T : new()
{
public static double CalcCpuUsage(double pTime, double elapsed)
{
double cpuUsage = 0;
if (elapsed > 0)
{
cpuUsage = (pTime / elapsed) / Environment.ProcessorCount * 100.0;
if (cpuUsage > 100.0) cpuUsage = 100.0;
}
return cpuUsage;
}
public AbstractCpuScanner(IScanProvider scanner, LazyConcurrentContainer<T> container)
: base(scanner, container) { }
}
}

View File

@ -0,0 +1,66 @@
// File: Services/Scanners/AbstractScanner.cs
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public abstract class AbstractScanner<T> : IScannable where T : new()
{
internal IScanProvider _provider;
LazyConcurrentContainer<T> _container;
abstract public ScanTarget Target { get; }
internal IScanProvider Provider { get => _provider; }
internal LazyConcurrentContainer<T> Container { get => _container; }
internal int Timeout { get => _provider.GetTimeout(Target); }
internal bool IsActual()
{
return (DateTime.UtcNow - Container.LastUpdate).TotalSeconds < Timeout;
}
internal bool GetFromCacheOrNew(long key, out T res)
{
var cache = Container.Container;
bool result = true;
T? found;
if (cache == null || !cache.TryGetValue(key, out found))
{
result = false;
found = new();
}
res = found;
return result;
}
abstract internal bool Scan(out Dictionary<long, T>? data);
public void Scan()
{
bool success = false;
if (Container.Container != null && IsActual()) return;
try
{
if (success = Scan(out var data))
{
Container.SetContainer(data);
}
}
finally
{
if (!success)
{
Container.CleanContainer();
}
}
}
public AbstractScanner(IScanProvider scanProvider, LazyConcurrentContainer<T> container)
{
_provider = scanProvider;
_container = container;
}
}
}

View File

@ -0,0 +1,11 @@
// File: Services/Scanners/IScanProvider.cs
namespace WebmrAPI.Services.Scanners
{
public interface IScanProvider
{
public bool ProcessReadAccess { get; }
public ILogger Logger { get; }
public int GetTimeout(ScanTarget target);
}
}

View File

@ -0,0 +1,10 @@
// File: Services/Scanners/IScannable.cs
namespace WebmrAPI.Services.Scanners
{
public interface IScannable
{
public ScanTarget Target { get; }
public void Scan();
}
}

View File

@ -0,0 +1,61 @@
// File: Services/Scanners/MemoryRegionScanner.cs
using WebmrAPI.Exceptions;
using WebmrAPI.Models;
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public class MemoryRegionScanner : AbstractScanner<MemoryRegionInfo>
{
override public ScanTarget Target { get => ScanTarget.MemoryRegions; }
internal int PID { get; private set; }
override internal bool Scan(out Dictionary<long, MemoryRegionInfo>? data)
{
data = new();
try
{
using (var process = new WindowsProcess(PID))
{
var regions = data;
process.ForeachMemoryRegion(info =>
{
long addr = info.BaseAddress.ToInt64();
regions.Add(addr, new MemoryRegionInfo
{
MemoryAddress = addr,
MemorySize = info.RegionSize.ToUInt64(),
MemoryState = info.State,
MemoryPageProtection = info.PageProtection,
MemoryType = info.Type
});
});
}
return true;
}
catch (ProcessAccessDeniedException ex)
{
Provider.Logger.LogWarning($"Access denied to process {PID} for memory region scanning. Error: {ex.Message}");
}
catch (MemoryRegionException ex)
{
Provider.Logger.LogWarning($"Error scanning memory regions for PID {PID}. Error: {ex.Message}");
return true;
}
catch (Exception ex)
{
Provider.Logger.LogError($"An unexpected error occurred while scanning memory regions for PID {PID}. Error: {ex.Message}");
}
return false;
}
public MemoryRegionScanner(IScanProvider scanner, LazyConcurrentContainer<MemoryRegionInfo> container, int pid)
: base(scanner, container)
{
PID = pid;
}
}
}

View File

@ -0,0 +1,98 @@
// File: Services/Scanners/ModuleScanner.cs
using System.Diagnostics;
using WebmrAPI.Exceptions;
using WebmrAPI.Models;
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public class ModuleScanner : AbstractScanner<ProcessModuleInfo>
{
override public ScanTarget Target { get => ScanTarget.Modules; }
internal int PID { get; private set; }
override internal bool Scan(out Dictionary<long, ProcessModuleInfo>? data)
{
data = new();
try
{
using (var process = Process.GetProcessById(PID))
{
foreach (ProcessModule module in process.Modules)
{
long addr = module.BaseAddress.ToInt64();
data.Add(addr, new ProcessModuleInfo
{
MemoryAddress = addr,
ModuleName = module.ModuleName,
FileName = module.FileName,
MemorySize = (ulong)module.ModuleMemorySize,
EntrypointRawAddress = module.EntryPointAddress.ToInt64()
});
}
}
return true;
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5)
{
Provider.Logger.LogWarning($"Access denied to Process.Modules for PID {PID}. Attempting P/Invoke. Error: {ex.Message}");
if (!Provider.ProcessReadAccess) return false;
}
catch (InvalidOperationException ex)
{
Provider.Logger.LogWarning($"Process with PID {PID} might have exited before module scanning. Error: {ex.Message}");
return false;
}
catch (Exception ex)
{
Provider.Logger.LogError(ex, $"An unexpected error occurred while getting modules for PID {PID} using managed API.");
return false;
}
try
{
using (var process = new WindowsProcess(PID, true))
{
var modules = data;
process.ForeachLoadedModule(info =>
{
long addr = info.BaseAddress.ToInt64();
modules.Add(addr, new ProcessModuleInfo
{
ModuleName = info.Name.ToString(),
MemoryAddress = addr,
FileName = info.FileName.ToString(),
MemorySize = info.MemorySize,
EntrypointRawAddress = info.EntryPointAddress.ToInt64()
});
});
}
return true;
}
catch (GettingModuleInfoException ex)
{
Provider.Logger.LogDebug(ex.Message);
}
catch (ProcessMonitorException ex)
{
Provider.Logger.LogError(ex.Message);
}
catch (Exception ex)
{
Provider.Logger.LogError(ex, $"An error occurred while getting modules for PID {PID} using P/Invoke.");
}
return false;
}
public ModuleScanner(IScanProvider scanner, LazyConcurrentContainer<ProcessModuleInfo> container, int pid)
: base(scanner, container)
{
PID = pid;
}
}
}

View File

@ -0,0 +1,148 @@
// File: Services/Scanners/ProcessScanner.cs
using System.Diagnostics;
using System.Management;
using WebmrAPI.Models;
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public class ProcessScanner : AbstractCpuScanner<ProcessInfo>
{
override public ScanTarget Target { get => ScanTarget.Processes; }
private Task<Dictionary<int, (string? Name, string? CommandLine, int ParentPID)>> _wmi;
private Dictionary<int, (string? Name, string? CommandLine, int ParentPID)> WmiData
{
get => _wmi.Result;
}
async private Task<Dictionary<int, (string? Name, string? CommandLine, int ParentPID)>> GetWmiDataAsync()
{
var wmi = new Dictionary<int, (string? Name, string? CommandLine, int ParentPID)>();
try
{
using (var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine, ParentProcessId FROM Win32_Process"))
using (var processes = await Task.Run(() => { return searcher.Get(); }))
await Task.Run(() =>
{
foreach (var 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();
wmi[pid] = (name, cmd, ppid);
}
});
}
catch (Exception ex)
{
Provider.Logger.LogError(ex, "Failed to retrieve WMI process data for all processes.");
}
return wmi;
}
private bool Populate(ProcessInfo info, Process process)
{
info.TotalProcessorTime = process.TotalProcessorTime;
info.MemorySize = (ulong)process.WorkingSet64;
info.ThreadCount = process.Threads.Count;
try
{
if (info.MemoryAddress == 0 && process.MainModule != null)
{
info.FileName = process.MainModule.FileName;
info.MemoryAddress = process.MainModule.BaseAddress.ToInt64();
}
}
catch (Exception ex)
{
Provider.Logger.LogWarning($"Process {process.ProcessName} (PID: {process.Id}), an error occurred while getting process address: {ex.Message}");
}
if (info.MemoryAddress != 0)
{
try
{
if (WmiData.TryGetValue(process.Id, out var entry))
{
info.Name = entry.Name;
info.CommandLine = entry.CommandLine;
info.ParentPID = entry.ParentPID;
}
else
{
info.Name = process.ProcessName;
Provider.Logger.LogDebug($"WMI data not found for PID {process.Id}. Falling back to Process.ProcessName.");
}
if (info.StartTime == null)
{
try { info.StartTime = process.StartTime; }
catch (Exception ex)
{
Provider.Logger.LogDebug($"Could not get StartTime for PID {process.Id}: {ex.Message}");
}
}
process.Refresh();
info.Status = process.Responding ? ProcessInfo.ProcessStatus.Running : ProcessInfo.ProcessStatus.NotResponding;
}
catch (Exception ex)
{
info.Status = ProcessInfo.ProcessStatus.Undefined;
Provider.Logger.LogDebug($"Could not get Status for PID {process.Id}: {ex.Message}");
}
return true;
}
return false;
}
override internal bool Scan(out Dictionary<long, ProcessInfo>? data)
{
data = new();
foreach (var process in Process.GetProcesses())
using (process)
{
ProcessInfo? info;
if (process.Id == 0 || process.Id == 4)
{
process.Dispose();
continue;
}
if (GetFromCacheOrNew(process.Id, out info))
{
info.CpuUsage = CalcCpuUsage(info.ProcessorTime, Container.Elapsed.TotalMilliseconds);
}
else
{
info.PID = process.Id;
}
try
{
if (Populate(info, process))
{
data.Add(info.PID, info);
}
} catch (Exception) { }
}
return true;
}
public ProcessScanner(IScanProvider scanner, LazyConcurrentContainer<ProcessInfo> container)
: base(scanner, container)
{
_wmi = GetWmiDataAsync();
}
}
}

View File

@ -0,0 +1,78 @@
// File: Services/Scanners/ScanProvider.cs
using WebmrAPI.Models;
namespace WebmrAPI.Services.Scanners
{
public class ScanProvider : IScanProvider
{
private ProcessMonitor _monitor;
public bool ProcessReadAccess { get => _monitor.Config.AllowProcessReadAccess; }
public ILogger Logger { get => _monitor.Logger; }
public int GetTimeout(ScanTarget target)
{
switch (target)
{
case ScanTarget.Processes: return _monitor.Config.MemoryRegionScanTimeout;
case ScanTarget.Modules: return _monitor.Config.MemoryRegionScanTimeout;
case ScanTarget.Threads: return _monitor.Config.MemoryRegionScanTimeout;
default: return _monitor.Config.ProcessScanInterval;
}
}
public IScannable CreateScanTask()
{
return new ProcessScanner(this, _monitor.Processes);
}
private IScannable CreateScanTask(ScanTarget target)
{
var scanner = new ScanQueue();
CreateScanTask().Scan();
var data = _monitor.Processes.Values;
if (data != null)
{
foreach (var process in data)
{
scanner.Add(CreateScanTask(process, target));
}
}
return scanner;
}
public IScannable CreateScanTask(ProcessInfo? process, ScanTarget target = ScanTarget.ProcessDetails)
{
var scanner = new ScanQueue();
if (target.HasFlag(ScanTarget.Processes))
{
return CreateScanTask(target^ScanTarget.Processes);
}
if (target.HasFlag(ScanTarget.MemoryRegions) && process != null)
{
scanner.Add(new MemoryRegionScanner(this, process.MemoryRegionsContainer, process.PID));
}
if (target.HasFlag(ScanTarget.Modules) && process != null)
{
scanner.Add(new ModuleScanner(this, process.ModulesContainer, process.PID));
}
if (target.HasFlag(ScanTarget.Threads) && process != null)
{
scanner.Add(new ThreadScanner(this, process.ThreadsContainer, process.PID));
}
return scanner;
}
public ScanProvider(ProcessMonitor monitoringService)
{
_monitor = monitoringService;
}
}
}

View File

@ -0,0 +1,27 @@
// File: Services/Scanners/ScanQueue.cs
namespace WebmrAPI.Services.Scanners
{
public class ScanQueue : IScannable
{
private ScanTarget _target = 0;
public ScanTarget Target { get => _target; }
private Queue<IScannable> _queue = new();
public ScanQueue Add(IScannable item)
{
_queue.Enqueue(item);
_target |= item.Target;
return this;
}
public void Scan()
{
while (_queue.Count > 0)
{
_queue.Dequeue().Scan();
}
}
}
}

View File

@ -0,0 +1,16 @@
// File: Services/Scanners/ScanTarget.cs
namespace WebmrAPI.Services.Scanners
{
[Flags]
public enum ScanTarget : byte
{
Processes = 0x01,
MemoryRegions = 0x02,
Modules = 0x04,
Threads = 0x08,
ProcessDetails = MemoryRegions | Modules | Threads,
All = Processes | ProcessDetails
}
}

View File

@ -0,0 +1,65 @@
// File: Services/Scanners/ThreadScanner.cs
using System.Diagnostics;
using WebmrAPI.Models;
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public class ThreadScanner : AbstractCpuScanner<ProcessThreadInfo>
{
override public ScanTarget Target { get => ScanTarget.Threads; }
internal int PID { get; private set; }
override internal bool Scan(out Dictionary<long, ProcessThreadInfo>? data)
{
data = new();
try
{
using (var process = Process.GetProcessById(PID))
foreach (ProcessThread thread in process.Threads)
{
ProcessThreadInfo info;
if (GetFromCacheOrNew(thread.Id, out info))
{
info.CpuUsage = CalcCpuUsage(info.ProcessorTime, Container.Elapsed.TotalMilliseconds);
}
else
{
info.ID = thread.Id;
}
info.BasePriority = thread.BasePriority;
info.CurrentPriority = thread.CurrentPriority;
info.TotalProcessorTime = thread.TotalProcessorTime;
data.Add(info.ID, info);
}
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5)
{
Provider.Logger.LogWarning($"Access denied to process {PID} for thread scanning. Error: {ex.Message}");
return false;
}
catch (InvalidOperationException ex)
{
Provider.Logger.LogWarning($"Process {PID} might have exited during thread enumeration. Error: {ex.Message}");
return false;
}
catch (Exception ex)
{
Provider.Logger.LogError($"An unexpected error occurred while scanning threads for PID {PID}. Error: {ex.Message}");
return false;
}
return true;
}
public ThreadScanner(IScanProvider scanner, LazyConcurrentContainer<ProcessThreadInfo> container, int pid)
: base(scanner, container)
{
PID = pid;
}
}
}

View File

@ -1,251 +0,0 @@
// File: Services/WinApi.cs
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.Json.Serialization;
using WebmrAPI.Utils;
namespace WebmrAPI.Services
{
[SupportedOSPlatform("windows")]
public static class WinApi
{
public const uint PROCESS_QUERY_INFORMATION = 0x00000400;
public const uint PROCESS_VM_READ = 0x00000010;
public const uint PROCESS_VM_OPERATION = 0x00000008;
public static int LastError { get => Marshal.GetLastWin32Error(); }
public static uint MBISize { get => (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)); }
[JsonConverter(typeof(JsonEnumConverter<MemoryState>))]
public enum MemoryState : uint
{
Undefined = 0,
Commit = 0x00001000,
Reserve = 0x00002000,
Free = 0x00010000,
}
[JsonConverter(typeof(JsonEnumConverter<MemoryType>))]
public enum MemoryType : uint
{
Undefined = 0,
Image = 0x01000000,
Mapped = 0x00040000,
Private = 0x00020000,
}
[JsonConverter(typeof(JsonEnumConverter<MemoryPageProtectionState>))]
public enum MemoryPageProtectionState : uint
{
Undefined = 0,
NoAccess = 0x0001,
ReadOnly = 0x0002,
ReadWrite = 0x0004,
WriteCopy = 0x0008,
Execute = 0x0010,
ExecuteRead = 0x0020,
ExecuteReadWrite = 0x0040,
ExecuteWriteCopy = 0x0080,
Guard = 0x0100,
NoCache = 0x0200,
WriteCombine = 0x0400,
}
// --- P/Invoke Declarations ---
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
[StructLayout(LayoutKind.Sequential)]
public struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public ushort PartitionId;
public UIntPtr RegionSize;
public MemoryState State;
public MemoryPageProtectionState PageProtection;
public MemoryType Type;
}
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
uint dwProcessId
);
// https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-virtualqueryex
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int VirtualQueryEx(
IntPtr hProcess,
IntPtr lpAddress,
out MEMORY_BASIC_INFORMATION lpBuffer,
uint dwLength
);
// --- Module-related P/Invokes (from psapi.dll and kernel32.dll) ---
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodulesex
[DllImport("psapi.dll", SetLastError = true)]
public static extern bool EnumProcessModulesEx(
IntPtr hProcess,
[Out] IntPtr[] lphModule, // Array to receive module handles
uint cb, // Size of the lphModule array, in bytes
out uint lpcbNeeded, // Number of bytes required to store all module handles
uint dwFilterFlag // Filter for modules (e.g., LIST_MODULES_ALL)
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw
[DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint GetModuleFileNameEx(
IntPtr hProcess,
IntPtr hModule,
[Out] System.Text.StringBuilder lpFilename,
uint nSize
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamew
[DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint GetModuleBaseName(
IntPtr hProcess,
IntPtr hModule,
[Out] System.Text.StringBuilder lpBaseName,
uint nSize
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-_moduleinfo
[StructLayout(LayoutKind.Sequential)]
public struct MODULEINFO
{
public IntPtr lpBaseOfDll;
public uint SizeOfImage;
public IntPtr EntryPoint;
}
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmoduleinformation
[DllImport("psapi.dll", SetLastError = true)]
public static extern bool GetModuleInformation(
IntPtr hProcess,
IntPtr hModule,
out MODULEINFO lpmodinfo,
uint cb
);
// --- Thread-related P/Invokes (from kernel32.dll and ntdll.dll) ---
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthread
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenThread(
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
uint dwThreadId
);
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationthread
[DllImport("ntdll.dll", SetLastError = true)]
public static extern int NtQueryInformationThread(
IntPtr ThreadHandle,
THREAD_INFO_CLASS ThreadInformationClass,
IntPtr ThreadInformation, // Pointer to buffer
uint ThreadInformationLength,
out uint ReturnLength
);
// Enum for ThreadInformationClass in NtQueryInformationThread
// https://ntdoc.m417z.com/threadinfoclass
public enum THREAD_INFO_CLASS : int
{
ThreadBasicInformation, // q: THREAD_BASIC_INFORMATION
ThreadTimes, // q: KERNEL_USER_TIMES
ThreadPriority, // s: KPRIORITY (requires SeIncreaseBasePriorityPrivilege)
ThreadBasePriority, // s: KPRIORITY
ThreadAffinityMask, // s: KAFFINITY
ThreadImpersonationToken, // s: HANDLE
ThreadDescriptorTableEntry, // q: DESCRIPTOR_TABLE_ENTRY (or WOW64_DESCRIPTOR_TABLE_ENTRY)
ThreadEnableAlignmentFaultFixup, // s: BOOLEAN
ThreadEventPair, // Obsolete
ThreadQuerySetWin32StartAddress, // qs: PVOID (requires THREAD_Set_LIMITED_INFORMATION)
ThreadZeroTlsCell, // s: ULONG // TlsIndex // 10
ThreadPerformanceCount, // q: LARGE_INTEGER
ThreadAmILastThread, // q: ULONG
ThreadIdealProcessor, // s: ULONG
ThreadPriorityBoost, // qs: ULONG
ThreadSetTlsArrayAddress, // s: ULONG_PTR
ThreadIsIoPending, // q: ULONG
ThreadHideFromDebugger, // q: BOOLEAN; s: void
ThreadBreakOnTermination, // qs: ULONG
ThreadSwitchLegacyState, // s: void // NtCurrentThread // NPX/FPU
ThreadIsTerminated, // q: ULONG // 20
ThreadLastSystemCall, // q: THREAD_LAST_SYSCALL_INFORMATION
ThreadIoPriority, // qs: IO_PRIORITY_HINT (requires SeIncreaseBasePriorityPrivilege)
ThreadCycleTime, // q: THREAD_CYCLE_TIME_INFORMATION (requires THREAD_QUERY_LIMITED_INFORMATION)
ThreadPagePriority, // qs: PAGE_PRIORITY_INFORMATION
ThreadActualBasePriority, // s: LONG (requires SeIncreaseBasePriorityPrivilege)
ThreadTebInformation, // q: THREAD_TEB_INFORMATION (requires THREAD_GET_CONTEXT + THREAD_SET_CONTEXT)
ThreadCSwitchMon, // Obsolete
ThreadCSwitchPmu, // Obsolete
ThreadWow64Context, // qs: WOW64_CONTEXT, ARM_NT_CONTEXT since 20H1
ThreadGroupInformation, // qs: GROUP_AFFINITY // 30
ThreadUmsInformation, // q: THREAD_UMS_INFORMATION // Obsolete
ThreadCounterProfiling, // q: BOOLEAN; s: THREAD_PROFILING_INFORMATION?
ThreadIdealProcessorEx, // qs: PROCESSOR_NUMBER; s: previous PROCESSOR_NUMBER on return
ThreadCpuAccountingInformation, // q: BOOLEAN; s: HANDLE (NtOpenSession) // NtCurrentThread // since WIN8
ThreadSuspendCount, // q: ULONG // since WINBLUE
ThreadHeterogeneousCpuPolicy, // q: KHETERO_CPU_POLICY // since THRESHOLD
ThreadContainerId, // q: GUID
ThreadNameInformation, // qs: THREAD_NAME_INFORMATION (requires THREAD_SET_LIMITED_INFORMATION)
ThreadSelectedCpuSets, // q: ULONG[]
ThreadSystemThreadInformation, // q: SYSTEM_THREAD_INFORMATION // 40
ThreadActualGroupAffinity, // q: GROUP_AFFINITY // since THRESHOLD2
ThreadDynamicCodePolicyInfo, // q: ULONG; s: ULONG (NtCurrentThread)
ThreadExplicitCaseSensitivity, // qs: ULONG; s: 0 disables, otherwise enables // (requires SeDebugPrivilege and PsProtectedSignerAntimalware)
ThreadWorkOnBehalfTicket, // ALPC_WORK_ON_BEHALF_TICKET // RTL_WORK_ON_BEHALF_TICKET_EX // NtCurrentThread
ThreadSubsystemInformation, // q: SUBSYSTEM_INFORMATION_TYPE // since REDSTONE2
ThreadDbgkWerReportActive, // s: ULONG; s: 0 disables, otherwise enables
ThreadAttachContainer, // s: HANDLE (job object) // NtCurrentThread
ThreadManageWritesToExecutableMemory, // MANAGE_WRITES_TO_EXECUTABLE_MEMORY // since REDSTONE3
ThreadPowerThrottlingState, // qs: POWER_THROTTLING_THREAD_STATE // since REDSTONE3 (set), WIN11 22H2 (query)
ThreadWorkloadClass, // THREAD_WORKLOAD_CLASS // since REDSTONE5 // 50
ThreadCreateStateChange, // since WIN11
ThreadApplyStateChange,
ThreadStrongerBadHandleChecks, // s: ULONG // NtCurrentThread // since 22H1
ThreadEffectiveIoPriority, // q: IO_PRIORITY_HINT
ThreadEffectivePagePriority, // q: ULONG
ThreadUpdateLockOwnership, // THREAD_LOCK_OWNERSHIP // since 24H2
ThreadSchedulerSharedDataSlot, // SCHEDULER_SHARED_DATA_SLOT_INFORMATION
ThreadTebInformationAtomic, // q: THREAD_TEB_INFORMATION (requires THREAD_GET_CONTEXT + THREAD_QUERY_INFORMATION)
ThreadIndexInformation, // THREAD_INDEX_INFORMATION
MaxThreadInfoClass
}
// Structure for ThreadBasicInformation (if needed, though ThreadQuerySetWin32StartAddress directly gives address)
// https://ntdoc.m417z.com/thread_basic_information
[StructLayout(LayoutKind.Sequential)]
public struct THREAD_BASIC_INFORMATION
{
public int ExitStatus;
public IntPtr TebBaseAddress;
public CLIENT_ID ClientId;
public IntPtr AffinityMask;
public int Priority;
public int BasePriority;
}
// https://ntdoc.m417z.com/client_id
[StructLayout(LayoutKind.Sequential)]
public struct CLIENT_ID
{
public IntPtr UniqueProcess;
public IntPtr UniqueThread;
}
}
}

View File

@ -1,8 +1,8 @@
// File: Models/ConcurrentDTO.cs
// File: Utils/ConcurrentObject.cs
namespace WebmrAPI.Models
namespace WebmrAPI.Utils
{
public class ConcurrentDTO
public class ConcurrentObject
{
internal readonly object _lock = new object();

View File

@ -0,0 +1,55 @@
// File: Utils/LazyConcurrentContainer.cs
using System.Collections.Concurrent;
namespace WebmrAPI.Utils
{
public class LazyConcurrentContainer<T> : ConcurrentObject
{
private DateTime _lastScan = DateTime.MinValue;
private DateTime _currentScan = DateTime.MinValue;
private ConcurrentDictionary<long, T>? _container;
public DateTime LastUpdate
{
get => LockedGet(ref _lastScan);
}
public TimeSpan Elapsed
{
get { lock (_lock) return _currentScan - _lastScan; }
}
public ConcurrentDictionary<long, T>? Container
{
get => LockedGet(ref _container);
set
{
lock (_lock)
{
_lastScan = _currentScan;
_currentScan = DateTime.UtcNow;
_container = value;
}
}
}
public IEnumerable<T>? Values
{
get => _container?.Values.ToList();
}
public void SetContainer(Dictionary<long, T>? container)
{
Container = container != null ? new ConcurrentDictionary<long, T>(container) : null;
}
public void SetContainer(ConcurrentDictionary<long, T> container)
{
Container = container != null ? new ConcurrentDictionary<long, T>(container) : null;
}
public void CleanContainer()
{
Container = null;
}
}
}

248
Utils/WindowsProcess.cs Normal file
View File

@ -0,0 +1,248 @@
// File: Utils/WindowsProcess.cs
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.Json.Serialization;
using WebmrAPI.Exceptions;
using WebmrAPI.Models;
namespace WebmrAPI.Utils
{
[SupportedOSPlatform("windows")]
public class WindowsProcess : IDisposable
{
private IntPtr _ptr = IntPtr.Zero;
private int _pid = 0;
private bool _readAccess = false;
private static int LastError { get => Marshal.GetLastWin32Error(); }
private static uint MBISize { get => (uint)Marshal.SizeOf(typeof(MemoryBasicInformation)); }
[Flags]
[JsonConverter(typeof(JsonEnumConverter<MemoryState>))]
public enum MemoryState : uint
{
Commit = 0x00001000,
Reserve = 0x00002000,
Free = 0x00010000,
}
[Flags]
[JsonConverter(typeof(JsonEnumConverter<MemoryType>))]
public enum MemoryType : uint
{
Image = 0x01000000,
Mapped = 0x00040000,
Private = 0x00020000,
}
[Flags]
[JsonConverter(typeof(JsonEnumConverter<MemoryPageProtectionState>))]
public enum MemoryPageProtectionState : uint
{
NoAccess = 0x0001,
ReadOnly = 0x0002,
ReadWrite = 0x0004,
WriteCopy = 0x0008,
Execute = 0x0010,
ExecuteRead = 0x0020,
ExecuteReadWrite = 0x0040,
ExecuteWriteCopy = 0x0080,
Guard = 0x0100,
NoCache = 0x0200,
WriteCombine = 0x0400,
}
[Flags]
public enum ModuleFilterFlags : uint
{
Default = 0x00,
Modules32Bit = 0x01,
Modules64Bit = 0x02,
ModulesAll = Modules32Bit | Modules64Bit,
}
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
[StructLayout(LayoutKind.Sequential)]
public struct MemoryBasicInformation
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public ushort PartitionId;
public UIntPtr RegionSize;
public MemoryState State;
public MemoryPageProtectionState PageProtection;
public MemoryType Type;
}
[StructLayout(LayoutKind.Sequential)]
public struct ModuleInformation
{
public IntPtr BaseAddress;
public uint MemorySize;
public IntPtr EntryPointAddress;
public System.Text.StringBuilder Name;
public System.Text.StringBuilder FileName;
}
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
int dwProcessId
);
// https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-virtualqueryex
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualQueryEx(
IntPtr hProcess,
IntPtr lpAddress,
out MemoryBasicInformation lpBuffer,
uint dwLength
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodulesex
[DllImport("psapi.dll", SetLastError = true)]
private static extern bool EnumProcessModulesEx(
IntPtr hProcess,
[Out] IntPtr[] lphModule,
uint cb,
out uint lpcbNeeded,
ModuleFilterFlags dwFilterFlag
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexw
[DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint GetModuleFileNameEx(
IntPtr hProcess,
IntPtr hModule,
[Out] System.Text.StringBuilder lpFilename,
uint nSize
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamew
[DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint GetModuleBaseName(
IntPtr hProcess,
IntPtr hModule,
[Out] System.Text.StringBuilder lpBaseName,
uint nSize
);
// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmoduleinformation
[DllImport("psapi.dll", SetLastError = true)]
private static extern bool GetModuleInformation(
IntPtr hProcess,
IntPtr hModule,
out ModuleInformation lpmodinfo,
uint cb
);
public void ForeachMemoryRegion(Action<MemoryBasicInformation> callback, MemoryState filter = MemoryState.Commit | MemoryState.Reserve)
{
IntPtr cur = IntPtr.Zero;
long next = 0;
var regions = new Dictionary<long, MemoryRegionInfo>();
while ((next > cur.ToInt64() && next < 0x7fffffffffff) || next == 0)
{
MemoryBasicInformation info;
cur = new IntPtr(next);
if (VirtualQueryEx(_ptr, cur, out info, MBISize) == 0)
{
throw new MemoryRegionException($"WinAPI call 'VirtualQueryEx for PID {_pid} at address 0x{cur.ToInt64():X16}' failed.");
}
if ((filter & info.State) != 0)
{
callback(info);
}
next = info.BaseAddress.ToInt64() + (long)info.RegionSize.ToUInt64();
}
}
public void ForeachLoadedModule(Action<ModuleInformation> callback, ModuleFilterFlags filter = ModuleFilterFlags.ModulesAll, bool force = false, bool ignoreErrors = false)
{
if (!_readAccess && force)
{
Dispose();
_readAccess = true;
Open();
}
IntPtr[] modules = new IntPtr[1024];
uint nmemb;
if (EnumProcessModulesEx(_ptr, modules, (uint)(modules.Length * IntPtr.Size), out nmemb, filter))
{
uint count = nmemb / (uint)IntPtr.Size;
for (int i = 0; i < count; i++)
{
ModuleInformation info;
info.Name = new System.Text.StringBuilder(256);
info.FileName = new System.Text.StringBuilder(1024);
if ((!GetModuleInformation(_ptr, modules[i], out info, (uint)(2 * IntPtr.Size + sizeof(uint))) ||
GetModuleBaseName(_ptr, modules[i], info.Name, (uint)info.Name.Capacity) <= 0 ||
GetModuleFileNameEx(_ptr, modules[i], info.FileName, (uint)info.FileName.Capacity) <= 0) && !ignoreErrors)
{
throw new GettingModuleInfoException($"Failed to get module info for module handle {modules[i]} in PID {_pid}. Error: {LastError}");
}
callback(info);
}
}
else
{
throw new ProcessMonitorException($"EnumProcessModulesEx for PID {_pid} failed. Error: {LastError}");
}
}
private void Open()
{
var access = (uint)(_readAccess ? 0x00000010 : 0) | 0x00000400;
_ptr = OpenProcess(access, false, _pid);
if (_ptr == IntPtr.Zero)
{
var errno = LastError;
if (errno == 5)
{
throw new ProcessAccessDeniedException($"Access denied to process {_pid}");
}
else
{
throw new ProcessMonitorException($"WinAPI call 'OpenProcess for PID {_pid}' failed. Error: {errno}");
}
}
}
public WindowsProcess(int pid, bool readAccess = false)
{
_readAccess = readAccess;
_pid = pid;
Open();
}
public void Dispose()
{
if (_ptr != IntPtr.Zero) CloseHandle(_ptr);
}
~WindowsProcess()
{
Dispose();
}
}
}

View File

@ -14,7 +14,7 @@
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
<Company>OpenSource</Company>
<Product>Process Monitoring Agent</Product>
<Description>A service for detailed monitoring processes and memory regions.</Description>