This commit is contained in:
2025-07-03 15:22:40 +03:00
parent 09af9d5091
commit 802598be79
28 changed files with 1102 additions and 531 deletions
+23
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) { }
}
}
+66
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;
}
}
}
+11
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);
}
}
+10
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();
}
}
+61
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;
}
}
}
+98
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;
}
}
}
+148
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();
}
}
}
+78
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;
}
}
}
+27
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();
}
}
}
}
+16
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
}
}
+65
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;
}
}
}