v0.1.1
This commit is contained in:
parent
f9d6695d96
commit
87ba9aa4b8
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
/.vs/
|
||||
/bin/
|
||||
/obj/
|
||||
/Properties/
|
||||
/appsettings.Development.json
|
||||
/webmr-api.csproj.user
|
||||
|
@ -1,19 +1,21 @@
|
||||
// File: Configuration/AppSettings.cs
|
||||
|
||||
namespace WebmrAPI.Configuration
|
||||
{
|
||||
public class AppSettings
|
||||
{
|
||||
public AgentSettings Agent { get; set; } = new AgentSettings();
|
||||
public WebServerSettings WebServer { get; set; } = new WebServerSettings();
|
||||
public MonitoringSettings Monitoring { get; set; } = new MonitoringSettings();
|
||||
public WebServerSettings WebServer { get; set; } = new WebServerSettings();
|
||||
}
|
||||
|
||||
public class AgentSettings
|
||||
public class MonitoringSettings
|
||||
{
|
||||
public int ScanIntervalSeconds { get; set; } = 5; // Default scan every 5 seconds
|
||||
public string TargetProcessName { get; set; } = "example.exe"; // Default target process
|
||||
public int ProcessScanInterval { get; set; } = 5;
|
||||
public int MemoryRegionScanTimeout { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class WebServerSettings
|
||||
{
|
||||
public string Url { get; set; } = "http://0.0.0.0:8080"; // Default listening URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
82
Controllers/ProcessController.cs
Normal file
82
Controllers/ProcessController.cs
Normal file
@ -0,0 +1,82 @@
|
||||
// File: Controllers/ProcessController.cs
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using WebmrAPI.Models;
|
||||
using WebmrAPI.Services;
|
||||
|
||||
namespace WebmrAPI.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
public class ProcessController : ControllerBase
|
||||
{
|
||||
private readonly ProcessMonitor _monitor;
|
||||
private readonly ILogger<ProcessController> _logger;
|
||||
|
||||
public ProcessController(ProcessMonitor monitor, ILogger<ProcessController> logger)
|
||||
{
|
||||
_monitor = monitor;
|
||||
_logger = logger;
|
||||
}
|
||||
private string GetFormattedJson<T>(T data, bool pretty)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = pretty,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
return JsonSerializer.Serialize(data, options);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ProcessBaseInfo>))]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetProcesses([FromQuery] bool pretty = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = _monitor.GetBufferedProcesses();
|
||||
return Content(GetFormattedJson(processes, pretty), "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred when getting the list of processes");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "An internal server error occurred when receiving processes.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{pid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ProcessInfo))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetProcessById(int pid, [FromQuery] bool pretty = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = _monitor.GetProcessDetails(pid);
|
||||
if (data == null)
|
||||
{
|
||||
return NotFound($"The process with the PID {pid} was not found or its details could not be obtained.");
|
||||
}
|
||||
return Content(GetFormattedJson(data, pretty), "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"An error occurred while receiving process details for PID {pid}.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"An internal server error occurred while receiving process details for PID {pid}.");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Добавить эндпоинты для:
|
||||
// - /api/Process/{pid}/memory_regions
|
||||
// - /api/Process/{pid}/modules
|
||||
// - /api/Process/{pid}/threads
|
||||
// - Поиск по имени
|
||||
// - Пагинация
|
||||
// - Нагрузка на CPU
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace WebmrAPI.Exceptions
|
||||
{
|
||||
public class ProcessAccessDeniedException : Exception
|
||||
{
|
||||
public ProcessAccessDeniedException(string message) : base(message) { }
|
||||
public ProcessAccessDeniedException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
}
|
8
Exceptions/ProcessMonitorException.cs
Normal file
8
Exceptions/ProcessMonitorException.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace WebmrAPI.Exceptions
|
||||
{
|
||||
public class ProcessMonitorException : Exception
|
||||
{
|
||||
public ProcessMonitorException(string message) : base(message) { }
|
||||
public ProcessMonitorException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
}
|
19
Models/ConcurrentDTO.cs
Normal file
19
Models/ConcurrentDTO.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// File: Models/ConcurrentDTO.cs
|
||||
|
||||
namespace WebmrAPI.Models
|
||||
{
|
||||
public class ConcurrentDTO
|
||||
{
|
||||
internal readonly object _lock = new object();
|
||||
|
||||
public void LockedSet<T>(ref T dst, T value)
|
||||
{
|
||||
lock (_lock) dst = value;
|
||||
}
|
||||
|
||||
public T LockedGet<T>(ref T src)
|
||||
{
|
||||
lock (_lock) return src;
|
||||
}
|
||||
}
|
||||
}
|
28
Models/MemoryRegion.cs
Normal file
28
Models/MemoryRegion.cs
Normal file
@ -0,0 +1,28 @@
|
||||
// File: Models/MemoryRegion.cs
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebmrAPI.Models
|
||||
{
|
||||
public class MemoryRegion : ConcurrentDTO
|
||||
{
|
||||
private long _addr = 0;
|
||||
private ulong _size = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public long MemoryAddress
|
||||
{
|
||||
get => LockedGet(ref _addr);
|
||||
set => LockedSet(ref _addr, value);
|
||||
}
|
||||
public ulong MemorySize
|
||||
{
|
||||
get => LockedGet(ref _size);
|
||||
set => LockedSet(ref _size, value);
|
||||
}
|
||||
public string? BaseAddress
|
||||
{
|
||||
get => (MemoryAddress > 0) ? $"0x{MemoryAddress:X12}" : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,27 @@
|
||||
// File: Models/MemoryRegionInfo.cs
|
||||
|
||||
namespace WebmrAPI.Models
|
||||
{
|
||||
public class MemoryRegionInfo
|
||||
public class MemoryRegionInfo : MemoryRegion
|
||||
{
|
||||
public string BaseAddress { get; set; } = string.Empty;
|
||||
public long RegionSize { get; set; }
|
||||
public string State { get; set; } = string.Empty;
|
||||
public string Protect { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
private string _state = String.Empty;
|
||||
private string _protect = String.Empty;
|
||||
private string _type = String.Empty;
|
||||
|
||||
public string State
|
||||
{
|
||||
get => LockedGet(ref _state);
|
||||
set => LockedSet(ref _state, value);
|
||||
}
|
||||
public string Protect
|
||||
{
|
||||
get => LockedGet(ref _protect);
|
||||
set => LockedSet(ref _protect, value);
|
||||
}
|
||||
public string Type
|
||||
{
|
||||
get => LockedGet(ref _type);
|
||||
set => LockedSet(ref _type, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
Models/ProcessBaseInfo.cs
Normal file
85
Models/ProcessBaseInfo.cs
Normal file
@ -0,0 +1,85 @@
|
||||
// File: Models/ProcessBaseInfo.cs
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebmrAPI.Models
|
||||
{
|
||||
public enum ProcessStatus
|
||||
{
|
||||
Undefined,
|
||||
Running,
|
||||
NotResponding
|
||||
}
|
||||
|
||||
public class ProcessBaseInfo : MemoryRegion
|
||||
{
|
||||
private TimeSpan _lastPTime = TimeSpan.Zero;
|
||||
private TimeSpan _curPTime = TimeSpan.Zero;
|
||||
|
||||
private int _pid = 0;
|
||||
private int _ppid = -1;
|
||||
private int _tcount = 0;
|
||||
private string? _name = null;
|
||||
private string? _fname = null;
|
||||
private string? _cmd = null;
|
||||
private ProcessStatus _status = ProcessStatus.Undefined;
|
||||
private DateTime? _startTime = null;
|
||||
private double _cpuUsage = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public TimeSpan TotalProcessorTime
|
||||
{
|
||||
set { lock (_lock) { _lastPTime = _curPTime; _curPTime = value; } }
|
||||
}
|
||||
[JsonIgnore]
|
||||
public double ProcessorTime
|
||||
{
|
||||
get { lock (_lock) return (_lastPTime - _curPTime).TotalMilliseconds; }
|
||||
}
|
||||
public int PID
|
||||
{
|
||||
get => LockedGet(ref _pid);
|
||||
set => LockedSet(ref _pid, value);
|
||||
}
|
||||
public int ParentPID
|
||||
{
|
||||
get => LockedGet(ref _ppid);
|
||||
set => LockedSet(ref _ppid, value);
|
||||
}
|
||||
public string? Name
|
||||
{
|
||||
get => LockedGet(ref _name);
|
||||
set => LockedSet(ref _name, value);
|
||||
}
|
||||
public string? FileName
|
||||
{
|
||||
get => LockedGet(ref _fname);
|
||||
set => LockedSet(ref _fname, value);
|
||||
}
|
||||
public string? CommandLine
|
||||
{
|
||||
get => LockedGet(ref _cmd);
|
||||
set => LockedSet(ref _cmd, value);
|
||||
}
|
||||
public int ThreadCount
|
||||
{
|
||||
get => LockedGet(ref _tcount);
|
||||
set => LockedSet(ref _tcount, value);
|
||||
}
|
||||
public ProcessStatus Status
|
||||
{
|
||||
get => LockedGet(ref _status);
|
||||
set => LockedSet(ref _status, value);
|
||||
}
|
||||
public DateTime? StartTime
|
||||
{
|
||||
get => LockedGet(ref _startTime);
|
||||
set => LockedSet(ref _startTime, value);
|
||||
}
|
||||
public double CpuUsage
|
||||
{
|
||||
get => LockedGet(ref _cpuUsage);
|
||||
set => LockedSet(ref _cpuUsage, value);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,26 @@
|
||||
using System.Collections.Concurrent;
|
||||
// File: Models/ProcessInfo.cs
|
||||
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebmrAPI.Models
|
||||
{
|
||||
public class ProcessInfo
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class ProcessInfo : ProcessBaseInfo
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public int ProcessId { get; set; }
|
||||
public string ProcessName { get; set; } = string.Empty;
|
||||
public string? BaseAddress { get; set; }
|
||||
public long VirtualMemorySize { get; set; }
|
||||
public string? CommandLine { get; set; }
|
||||
|
||||
private DateTime _lastUpdate = DateTime.MinValue;
|
||||
private List<MemoryRegionInfo> _regions = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime LastUpdate
|
||||
{
|
||||
get => LockedGet(ref _lastUpdate);
|
||||
set => LockedSet(ref _lastUpdate, value);
|
||||
}
|
||||
public List<MemoryRegionInfo> MemoryRegions
|
||||
{
|
||||
get => LockedGet(ref _regions);
|
||||
set => LockedSet(ref _regions, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WebmrAPI.Services
|
||||
{
|
||||
public class ProcessModuleInfo
|
||||
{
|
||||
public string? ModuleName { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public string? BaseAddress { get; set; }
|
||||
public long MemorySize { get; set; }
|
||||
public string? EntrypointAddress { get; set; }
|
||||
|
||||
public static ProcessModuleInfo FromProcessModule(ProcessModule module)
|
||||
{
|
||||
return new ProcessModuleInfo
|
||||
{
|
||||
ModuleName = module.ModuleName,
|
||||
FileName = module.FileName,
|
||||
BaseAddress = "0x" + module.BaseAddress.ToInt64().ToString("X"),
|
||||
MemorySize = module.ModuleMemorySize,
|
||||
EntrypointAddress = "0x" + module.EntryPointAddress.ToInt64().ToString("X")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
61
Program.cs
61
Program.cs
@ -1,59 +1,46 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
// File: Program.cs
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.Versioning;
|
||||
using WebmrAPI.Configuration;
|
||||
using WebmrAPI.Models; // Äîáàâëÿåì using äëÿ ìîäåëåé
|
||||
using WebmrAPI.Services;
|
||||
|
||||
[assembly: SupportedOSPlatform("windows")]
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Çàãðóçêà êîíôèãóðàöèè
|
||||
builder.Services.Configure<AppSettings>(builder.Configuration);
|
||||
|
||||
// Äîáàâëÿåì ëîãèðîâàíèå
|
||||
builder.Services.AddLogging(config =>
|
||||
{
|
||||
config.AddConsole();
|
||||
config.AddDebug();
|
||||
});
|
||||
|
||||
// Ðåãèñòðàöèÿ ProcessMonitor êàê Singleton è êàê IHostedService
|
||||
builder.Services.AddRouting(options =>
|
||||
{
|
||||
options.LowercaseUrls = true;
|
||||
options.LowercaseQueryStrings = true;
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddSingleton<ProcessMonitor>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ProcessMonitor>());
|
||||
|
||||
// Ðåãèñòðàöèÿ WinApi êàê Singleton (èëè ìîæíî ñäåëàòü åãî ñòàòè÷åñêèì, êàê ñåé÷àñ)
|
||||
// Åñëè WinApi ñòàíåò íåñòàòè÷åñêèì è áóäåò èìåòü çàâèñèìîñòè, ðåãèñòðèðóéòå åãî çäåñü:
|
||||
// builder.Services.AddSingleton<WinApi>(); // Åñëè ýòî êëàññ, à íå ñòàòè÷åñêèé
|
||||
// ---------------------------------------------------------------------------------------------------------------------------- \\
|
||||
|
||||
var app = builder.Build();
|
||||
var app = builder.Build();
|
||||
var settings = app.Services.GetRequiredService<IOptions<AppSettings>>().Value;
|
||||
|
||||
// Ïðèâÿçûâàåì URL èç íàñòðîåê
|
||||
var appSettings = app.Services.GetRequiredService<IOptions<AppSettings>>().Value;
|
||||
app.Urls.Add(appSettings.WebServer.Url);
|
||||
app.Urls.Add(settings.WebServer.Url);
|
||||
|
||||
|
||||
// GET /api/processes
|
||||
app.MapGet("/api/processes", (ProcessMonitor monitor, [FromQuery] bool pretty = false) =>
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
return Results.Content(JsonSerializer.Serialize(monitor.Processes, new JsonSerializerOptions { WriteIndented = pretty }), "application/json");
|
||||
});
|
||||
|
||||
// GET /api/processes/{pid}/memory_regions
|
||||
app.MapGet("/api/processes/{pid}/memory_regions", (int pid, ProcessMonitor monitor, [FromQuery] bool pretty = false) =>
|
||||
{
|
||||
var regions = monitor.GetBufferedMemoryRegions(pid);
|
||||
if (regions == null) {
|
||||
return Results.NotFound($"Process with PID {pid} or its memory regions not found.");
|
||||
}
|
||||
return Results.Content(JsonSerializer.Serialize(regions, new JsonSerializerOptions { WriteIndented = pretty }), "application/json");
|
||||
});
|
||||
|
||||
// GET /api/status/last_modified
|
||||
app.MapGet("/api/status/last_modified", (ProcessMonitor monitor, [FromQuery] bool pretty = false) =>
|
||||
{
|
||||
var date = new { LastModified = monitor.LastModifiedTimestamp.ToString("o") };
|
||||
return Results.Content(JsonSerializer.Serialize(date, new JsonSerializerOptions { WriteIndented = pretty }), "application/json");
|
||||
});
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
|
@ -1,438 +1,74 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
// File: Services/ProcessMonitor.cs
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using WebmrAPI.Configuration;
|
||||
using WebmrAPI.Exceptions;
|
||||
using WebmrAPI.Models;
|
||||
|
||||
namespace WebmrAPI.Services
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class ProcessMonitor : IHostedService, IDisposable
|
||||
{
|
||||
private DateTime _modifyTimestamp = DateTime.UtcNow;
|
||||
private DateTime _lastScanTime = DateTime.MinValue;
|
||||
private DateTime _currentScanTime = DateTime.MinValue;
|
||||
|
||||
private Timer? _timer;
|
||||
private readonly ILogger<ProcessMonitor> _logger;
|
||||
private readonly AgentSettings _agentSettings;
|
||||
private readonly MonitoringSettings _config;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
private ConcurrentDictionary<int, ProcessInfo> _processesBuffer = new();
|
||||
private ConcurrentDictionary<int, List<MemoryRegionInfo>> _memoryRegionsBuffer = new();
|
||||
|
||||
|
||||
public DateTime LastModifiedTimestamp
|
||||
private DateTime ScanTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _modifyTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private set
|
||||
set
|
||||
{
|
||||
_modifyTimestamp = value;
|
||||
_lastScanTime = _currentScanTime;
|
||||
_currentScanTime = value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ProcessInfo> Processes
|
||||
private TimeSpan Elapsed
|
||||
{
|
||||
get
|
||||
get => _currentScanTime - _lastScanTime;
|
||||
}
|
||||
|
||||
public IEnumerable<ProcessBaseInfo> GetBufferedProcesses()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock)
|
||||
return _processesBuffer.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public ProcessInfo? GetProcessDetails(int pid)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_processesBuffer.TryGetValue(pid, out ProcessInfo? data))
|
||||
{
|
||||
return _processesBuffer.Values.ToList();
|
||||
return PopulateProcessInfo(data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> appSettings)
|
||||
public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_agentSettings = appSettings.Value.Agent;
|
||||
_config = settings.Value.Monitoring;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("ProcessMonitor started. Scan interval: {ScanIntervalSeconds} seconds.", _agentSettings.ScanIntervalSeconds);
|
||||
// Çàïóñêàåì ïåðâûé ñêàí ñðàçó, çàòåì ïî òàéìåðó
|
||||
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_agentSettings.ScanIntervalSeconds));
|
||||
_logger.LogInformation($"ProcessMonitor started. Scan interval: {_config.ProcessScanInterval} seconds.");
|
||||
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_config.ProcessScanInterval));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void DoWork(object? state)
|
||||
{
|
||||
_logger.LogDebug("Initiating process scan...");
|
||||
try
|
||||
{
|
||||
var currentProcesses = new Dictionary<int, ProcessInfo>();
|
||||
var currentMemoryRegions = new Dictionary<int, List<MemoryRegionInfo>>();
|
||||
bool changedDetected = false;
|
||||
|
||||
// --- 1. Get all running processes and their command lines using WMI ---
|
||||
// WMI is more reliable for getting command line of arbitrary processes
|
||||
var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine FROM Win32_Process");
|
||||
Dictionary<int, string> wmiCommandLines = new Dictionary<int, string>();
|
||||
Dictionary<int, string> wmiProcessNames = new Dictionary<int, string>();
|
||||
|
||||
foreach (ManagementObject queryObj in searcher.Get())
|
||||
{
|
||||
int pid = Convert.ToInt32(queryObj["ProcessId"]);
|
||||
string? commandLine = queryObj["CommandLine"]?.ToString();
|
||||
string? processName = queryObj["Name"]?.ToString();
|
||||
|
||||
wmiCommandLines[pid] = commandLine ?? string.Empty;
|
||||
wmiProcessNames[pid] = processName ?? string.Empty;
|
||||
}
|
||||
|
||||
// --- 2. Iterate through System.Diagnostics.Process objects ---
|
||||
System.Diagnostics.Process[] allProcesses = System.Diagnostics.Process.GetProcesses();
|
||||
|
||||
foreach (var process in allProcesses)
|
||||
{
|
||||
// Èñêëþ÷àåì ñèñòåìíûå ïðîöåññû, êîòîðûå íå èìåþò îáû÷íûõ ìîäóëåé/ïàìÿòè
|
||||
if (process.Id == 0 || process.Id == 4)
|
||||
{
|
||||
_logger.LogDebug("Skipping special system process {ProcessName} (PID: {PID}).", process.ProcessName, process.Id);
|
||||
process.Dispose(); // Âñåãäà îñâîáîæäàåì Process îáúåêò
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string processName = wmiProcessNames.GetValueOrDefault(process.Id, process.ProcessName); // Prefer WMI name if available
|
||||
string commandLine = wmiCommandLines.GetValueOrDefault(process.Id, string.Empty);
|
||||
|
||||
var processInfo = new ProcessInfo
|
||||
{
|
||||
ProcessId = process.Id,
|
||||
ProcessName = processName,
|
||||
VirtualMemorySize = process.VirtualMemorySize64,
|
||||
CommandLine = commandLine
|
||||
};
|
||||
|
||||
// Try to get BaseAddress from MainModule. This can still throw Access Denied.
|
||||
try
|
||||
{
|
||||
// Some processes (e.g., system processes like Idle, System) might not have a MainModule
|
||||
if (process.MainModule != null)
|
||||
{
|
||||
processInfo.BaseAddress = $"0x{process.MainModule.BaseAddress.ToInt64():X}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Process {ProcessName} (PID: {PID}) has no MainModule.", processName, process.Id);
|
||||
processInfo.BaseAddress = null; // Ensure it's explicitly null if not found
|
||||
}
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5) // Access Denied
|
||||
{
|
||||
// Îøèáêà 5: Access Denied. Îøèáêà 0x80004005 (êîòîðàÿ òàêæå ìîæåò áûòü Win32Exception ñ îáùèì êîäîì)
|
||||
if (ex.NativeErrorCode == 5)
|
||||
{
|
||||
_logger.LogWarning("Access denied to MainModule for process {ProcessName} (PID: {PID}). BaseAddress will be null. Error: {ErrorMessage}", processName, process.Id, ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Äëÿ äðóãèõ Win32Exception, òàêèõ êàê "Unable to enumerate the process modules." (0x80004005)
|
||||
_logger.LogWarning(ex, "Win32Exception getting MainModule for process {ProcessName} (PID: {PID}). BaseAddress will be null.", processName, process.Id);
|
||||
}
|
||||
processInfo.BaseAddress = null;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Could not get MainModule for process {ProcessName} (PID: {PID}). Process might have exited or has no MainModule.", processName, process.Id);
|
||||
processInfo.BaseAddress = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error getting MainModule for process {ProcessName} (PID: {PID}).", processName, process.Id);
|
||||
processInfo.BaseAddress = null;
|
||||
}
|
||||
|
||||
|
||||
currentProcesses.Add(process.Id, processInfo);
|
||||
|
||||
// --- 3. Scan memory regions for the target process OR new processes ---
|
||||
bool isTargetProcess = _agentSettings.TargetProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// We scan memory regions for:
|
||||
// 1. The specific target process (if _agentSettings.TargetProcessName is set)
|
||||
// 2. ALL processes if _agentSettings.TargetProcessName is empty/null
|
||||
// 3. Any newly detected process that wasn't in our previous buffer (so we get its initial memory layout)
|
||||
if (isTargetProcess || string.IsNullOrEmpty(_agentSettings.TargetProcessName) || !_processesBuffer.ContainsKey(process.Id))
|
||||
{
|
||||
try
|
||||
{
|
||||
var regions = ScanMemoryRegionsForProcess(process);
|
||||
if (regions.Any())
|
||||
{
|
||||
currentMemoryRegions.Add(process.Id, regions);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No memory regions found or accessible for process {ProcessName} (PID: {PID}).", processName, process.Id);
|
||||
}
|
||||
}
|
||||
catch (ProcessAccessDeniedException ex)
|
||||
{
|
||||
// Îïðåäåëÿåì ñïèñîê ñèñòåìíûõ ïðîöåññîâ, êîòîðûå îáû÷íî îòêàçûâàþò â äîñòóïå
|
||||
string[] commonSystemProcesses = {
|
||||
"System", "System Idle Process", "Registry", "smss.exe", "csrss.exe",
|
||||
"wininit.exe", "services.exe", "lsass.exe", "winlogon.exe", "fontdrvhost.exe",
|
||||
"Memory Compression", "dwm.exe", "RuntimeBroker.exe", // Common UWP/System processes
|
||||
"msrdc.exe", "svchost.exe", // svchost can be varied, but often restricted
|
||||
"MsMpEng.exe", "MpDefenderCoreService.exe" // Windows Defender related
|
||||
};
|
||||
|
||||
// Ïðîâåðÿåì, ÿâëÿåòñÿ ëè ïðîöåññ îäíèì èç îáû÷íî çàùèùåííûõ
|
||||
if (commonSystemProcesses.Contains(processName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug(ex, "Access denied to memory regions for known system process {ProcessName} (PID: {PID}). This is expected.", processName, process.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Access denied to memory regions for process {ProcessName} (PID: {PID}). Skipping memory scan for it. Elevated privileges needed?", processName, process.Id);
|
||||
}
|
||||
}
|
||||
// Other exceptions from ScanMemoryRegionsForProcess will be caught by outer catch
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// Process might have exited between GetProcesses and accessing its properties
|
||||
_logger.LogDebug(ex, "Process {ProcessName} (PID: {PID}) exited during scan. Skipping.", process.ProcessName, process.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error scanning process {ProcessName} (PID: {PID}) for basic info or WMI data.", process.ProcessName, process.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Dispose the Process object to free resources associated with the handle
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Compare with buffered data and update if changes detected ---
|
||||
lock (_lock)
|
||||
{
|
||||
// Check for changes in processes (new, exited, basic info changed)
|
||||
if (!AreProcessInfosEqual(_processesBuffer.Values, currentProcesses.Values))
|
||||
{
|
||||
changedDetected = true;
|
||||
_logger.LogInformation("Process list or basic info changed.");
|
||||
}
|
||||
|
||||
// Check for changes in memory regions for monitored processes
|
||||
// Only compare if currentMemoryRegions contains the PID
|
||||
foreach (var kvp in currentMemoryRegions)
|
||||
{
|
||||
// If process was previously monitored AND its regions are different OR if it's a new process with regions
|
||||
if (!_memoryRegionsBuffer.ContainsKey(kvp.Key) || !AreMemoryRegionsEqual(_memoryRegionsBuffer[kvp.Key], kvp.Value))
|
||||
{
|
||||
changedDetected = true;
|
||||
_logger.LogInformation("Memory regions for PID {PID} changed.", kvp.Key);
|
||||
break; // No need to check further, change detected
|
||||
}
|
||||
}
|
||||
|
||||
// If a process disappeared (was in old buffer but not in new), it also counts as a change
|
||||
if (_processesBuffer.Count != currentProcesses.Count)
|
||||
{
|
||||
// If counts are different, and we haven't already marked as changed by content, mark as changed
|
||||
// This handles processes exiting.
|
||||
if (!changedDetected)
|
||||
{ // Only set if not already set by content change
|
||||
changedDetected = true;
|
||||
_logger.LogInformation("Process count changed (some processes might have exited).");
|
||||
}
|
||||
}
|
||||
|
||||
if (changedDetected)
|
||||
{
|
||||
_processesBuffer = new ConcurrentDictionary<int, ProcessInfo>(currentProcesses);
|
||||
_memoryRegionsBuffer = new ConcurrentDictionary<int, List<MemoryRegionInfo>>(currentMemoryRegions);
|
||||
LastModifiedTimestamp = DateTime.UtcNow;
|
||||
_logger.LogInformation("Buffered data updated. New LastModifiedTimestamp: {Timestamp}", LastModifiedTimestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No changes detected in processes or monitored memory regions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled error during process monitoring cycle.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ñêàíèðóåò ðåãèîíû ïàìÿòè äëÿ êîíêðåòíîãî ïðîöåññà.
|
||||
/// </summary>
|
||||
private List<MemoryRegionInfo> ScanMemoryRegionsForProcess(System.Diagnostics.Process process)
|
||||
{
|
||||
var regions = new List<MemoryRegionInfo>();
|
||||
IntPtr processHandle = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
// Open process with necessary access rights
|
||||
processHandle = WinApi.OpenProcess(
|
||||
WinApi.PROCESS_QUERY_INFORMATION | WinApi.PROCESS_VM_OPERATION | WinApi.PROCESS_VM_READ, // We also need VM_READ for VirtualQueryEx if it needs to access pages
|
||||
false, // Do not inherit handle
|
||||
(uint)process.Id
|
||||
);
|
||||
|
||||
if (processHandle == IntPtr.Zero)
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
if (errorCode == 5) // ERROR_ACCESS_DENIED
|
||||
{
|
||||
throw new ProcessAccessDeniedException($"Access denied to process {process.ProcessName} (PID: {process.Id}). Error code: {errorCode}");
|
||||
}
|
||||
WinApi.ThrowWinApiErrorIfAny($"OpenProcess for PID {process.Id}");
|
||||
return regions; // Should not reach here if ThrowWinApiErrorIfAny throws
|
||||
}
|
||||
|
||||
IntPtr currentAddress = IntPtr.Zero;
|
||||
long maxAddress = long.MaxValue; // Scan full 64-bit address space
|
||||
|
||||
if (Environment.Is64BitProcess)
|
||||
{
|
||||
// On 64-bit, the user-mode address space typically ends at 0x7FFFFFFFFFFF
|
||||
// No, maxAddress should be dynamic if needed, but for iteration it means "until VirtualQueryEx returns 0"
|
||||
// Or 0x7FFFFFFFFFFF (2^47 -1, which is the user-mode address space limit on 64-bit Windows)
|
||||
// For now, let VirtualQueryEx drive it.
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (currentAddress.ToInt64() >= (long)0x7FFFFFFFFFFF)
|
||||
{
|
||||
_logger.LogDebug("Reached effective end of 64-bit user-mode address space (0x{Address:X}) for PID {PID}.", currentAddress.ToInt64(), process.Id);
|
||||
break;
|
||||
}
|
||||
|
||||
WinApi.MEMORY_BASIC_INFORMATION mbi;
|
||||
// dwLength is the size of the buffer we pass in
|
||||
int result = WinApi.VirtualQueryEx(processHandle, currentAddress, out mbi, (uint)Marshal.SizeOf(typeof(WinApi.MEMORY_BASIC_INFORMATION)));
|
||||
|
||||
if (result == 0) // VirtualQueryEx returns 0 on failure or end of address space
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
// ERROR_NO_MORE_FILES (18) - îæèäàåìî â êîíöå àäðåñíîãî ïðîñòðàíñòâà
|
||||
// ERROR_INVALID_PARAMETER (87) - ÷àñòî âîçíèêàåò íà âûñîêèõ àäðåñàõ èëè íåâàëèäíûõ
|
||||
if (errorCode != 0 && errorCode != 18 && errorCode != 87) // Äîáàâëÿåì 87 êàê îæèäàåìîå çàâåðøåíèå
|
||||
{
|
||||
_logger.LogWarning("VirtualQueryEx for PID {PID} at address 0x{Address:X} failed with unexpected error code: {ErrorCode}.", process.Id, currentAddress.ToInt64(), errorCode);
|
||||
}
|
||||
else if (errorCode == 87)
|
||||
{
|
||||
_logger.LogDebug("VirtualQueryEx for PID {PID} at address 0x{Address:X} failed with ERROR_INVALID_PARAMETER (87). End of scannable address space reached.", process.Id, currentAddress.ToInt64());
|
||||
}
|
||||
break; // End of address space or an unrecoverable error
|
||||
}
|
||||
|
||||
// Only add committed or reserved regions
|
||||
if (mbi.State == WinApi.MEM_COMMIT || mbi.State == WinApi.MEM_RESERVE)
|
||||
{
|
||||
regions.Add(new MemoryRegionInfo
|
||||
{
|
||||
BaseAddress = $"0x{mbi.BaseAddress.ToInt64():X}",
|
||||
RegionSize = (long)mbi.RegionSize.ToUInt64(),
|
||||
State = WinApi.GetMemoryStateString(mbi.State),
|
||||
Protect = WinApi.GetMemoryProtectString(mbi.Protect),
|
||||
Type = WinApi.GetMemoryTypeString(mbi.Type)
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate next address to query from (BaseAddress + RegionSize)
|
||||
// Ensure we don't overflow or loop indefinitely on bad data
|
||||
long nextAddressLong = mbi.BaseAddress.ToInt64() + (long)mbi.RegionSize.ToUInt64();
|
||||
if (nextAddressLong <= currentAddress.ToInt64()) // Handle potential infinite loop if RegionSize is 0 or negative
|
||||
{
|
||||
_logger.LogWarning("VirtualQueryEx for PID {PID} advanced by non-positive amount. Breaking loop.", process.Id);
|
||||
break;
|
||||
}
|
||||
currentAddress = new IntPtr(nextAddressLong);
|
||||
|
||||
// Stop if we hit max address or overflow (which shouldn't happen with long.MaxValue for 64-bit)
|
||||
if (nextAddressLong >= maxAddress && maxAddress != long.MaxValue) break; // In case we ever set a specific maxAddress
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (processHandle != IntPtr.Zero)
|
||||
{
|
||||
WinApi.CloseHandle(processHandle);
|
||||
}
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ñðàâíèâàåò äâà ñïèñêà ProcessInfo äëÿ âûÿâëåíèÿ èçìåíåíèé.
|
||||
/// </summary>
|
||||
private bool AreProcessInfosEqual(ICollection<ProcessInfo> oldList, ICollection<ProcessInfo> newList)
|
||||
{
|
||||
if (oldList.Count != newList.Count) return false;
|
||||
|
||||
var oldDict = oldList.ToDictionary(p => p.ProcessId);
|
||||
var newDict = newList.ToDictionary(p => p.ProcessId);
|
||||
|
||||
foreach (var newProc in newDict.Values)
|
||||
{
|
||||
if (!oldDict.TryGetValue(newProc.ProcessId, out var oldProc)) return false; // New process or process ID changed
|
||||
|
||||
// Compare relevant fields. BaseAddress and CommandLine might change if process restarts or for some apps.
|
||||
// We consider them part of "basic info" that signals a change.
|
||||
if (oldProc.ProcessName != newProc.ProcessName ||
|
||||
oldProc.BaseAddress != newProc.BaseAddress ||
|
||||
oldProc.VirtualMemorySize != newProc.VirtualMemorySize ||
|
||||
oldProc.CommandLine != newProc.CommandLine)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ñðàâíèâàåò äâà ñïèñêà MemoryRegionInfo äëÿ âûÿâëåíèÿ èçìåíåíèé.
|
||||
/// </summary>
|
||||
private bool AreMemoryRegionsEqual(List<MemoryRegionInfo> oldList, List<MemoryRegionInfo> newList)
|
||||
{
|
||||
if (oldList.Count != newList.Count) return false;
|
||||
|
||||
// Sort by BaseAddress to ensure consistent comparison order
|
||||
var oldSorted = oldList.OrderBy(r => r.BaseAddress).ToList();
|
||||
var newSorted = newList.OrderBy(r => r.BaseAddress).ToList();
|
||||
|
||||
for (int i = 0; i < oldSorted.Count; i++)
|
||||
{
|
||||
var oldRegion = oldSorted[i];
|
||||
var newRegion = newSorted[i];
|
||||
|
||||
if (oldRegion.BaseAddress != newRegion.BaseAddress ||
|
||||
oldRegion.RegionSize != newRegion.RegionSize ||
|
||||
oldRegion.State != newRegion.State ||
|
||||
oldRegion.Protect != newRegion.Protect ||
|
||||
oldRegion.Type != newRegion.Type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("ProcessMonitor is stopping.");
|
||||
@ -440,21 +76,239 @@ 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.MEM_COMMIT || mbi.State == WinApi.MEM_RESERVE)
|
||||
{
|
||||
regions.Add(new MemoryRegionInfo
|
||||
{
|
||||
MemoryAddress = mbi.BaseAddress.ToInt64(),
|
||||
MemorySize = mbi.RegionSize.ToUInt64(),
|
||||
State = WinApi.GetStateString(mbi.State),
|
||||
Protect = WinApi.GetProtectString(mbi.Protect),
|
||||
Type = WinApi.GetTypeString(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.VirtualMemorySize64;
|
||||
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();
|
||||
}
|
||||
|
||||
public List<MemoryRegionInfo>? GetBufferedMemoryRegions(int pid)
|
||||
{
|
||||
lock (_lock) // Ensure thread-safe access
|
||||
{
|
||||
if (_memoryRegionsBuffer.TryGetValue(pid, out var regions))
|
||||
{
|
||||
return new List<MemoryRegionInfo>(regions); // Return a copy
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,60 @@
|
||||
// File: Services/WinApi.cs
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using WebmrAPI.Exceptions;
|
||||
|
||||
namespace WebmrAPI.Services
|
||||
{
|
||||
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;
|
||||
|
||||
// Ôëàãè äîñòóïà ê ïðîöåññó äëÿ OpenProcess
|
||||
public const uint PROCESS_QUERY_INFORMATION = 0x0400; // Required to retrieve certain information about a process, such as its token, exit code, and priority class.
|
||||
public const uint PROCESS_VM_READ = 0x0010; // Required to read memory in a process.
|
||||
public const uint PROCESS_VM_OPERATION = 0x0008; // Required to perform an operation on the address space of a process (e.g., VirtualQueryEx).
|
||||
public const uint MEM_COMMIT = 0x00001000;
|
||||
public const uint MEM_FREE = 0x00010000;
|
||||
public const uint MEM_RESERVE = 0x00002000;
|
||||
|
||||
// Ñîñòîÿíèÿ ïàìÿòè
|
||||
public const uint MEM_COMMIT = 0x1000; // Allocated and committed
|
||||
public const uint MEM_FREE = 0x10000; // Free (not reserved or committed)
|
||||
public const uint MEM_RESERVE = 0x2000; // Reserved
|
||||
public const uint MEM_IMAGE = 0x01000000;
|
||||
public const uint MEM_MAPPED = 0x00040000;
|
||||
public const uint MEM_PRIVATE = 0x00020000;
|
||||
|
||||
// Òèïû ïàìÿòè
|
||||
public const uint MEM_IMAGE = 0x1000000; // Mapped into the view of an image section
|
||||
public const uint MEM_MAPPED = 0x40000; // Mapped into the view of a data section
|
||||
public const uint MEM_PRIVATE = 0x20000; // Private (non-shared)
|
||||
public static int LastError { get => Marshal.GetLastWin32Error(); }
|
||||
public static uint MBISize { get => (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)); }
|
||||
|
||||
// --- Ñòðóêòóðû äàííûõ ---
|
||||
private static string GetHexValue(uint data)
|
||||
{
|
||||
return $"0x{data:X8}";
|
||||
}
|
||||
|
||||
public static string GetStateString(uint state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case MEM_COMMIT: return "MEM_COMMIT";
|
||||
case MEM_FREE: return "MEM_FREE";
|
||||
case MEM_RESERVE: return "MEM_RESERVE";
|
||||
default: return GetHexValue(state);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetProtectString(uint protect)
|
||||
{
|
||||
switch (protect)
|
||||
{
|
||||
default: return GetHexValue(protect);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetTypeString(uint type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MEM_IMAGE: return "MEM_IMAGE";
|
||||
case MEM_MAPPED: return "MEM_MAPPED";
|
||||
case MEM_PRIVATE: return "MEM_PRIVATE";
|
||||
default: return GetHexValue(type);
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
@ -31,14 +63,13 @@ namespace WebmrAPI.Services
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public ushort PartitionId; // Added in Windows 10, version 1709
|
||||
public UIntPtr RegionSize; // Use UIntPtr for size, as it's pointer-sized
|
||||
public ushort PartitionId;
|
||||
public UIntPtr RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
// --- P/Invoke Îáúÿâëåíèÿ Ôóíêöèé ---
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
@ -61,58 +92,5 @@ namespace WebmrAPI.Services
|
||||
out MEMORY_BASIC_INFORMATION lpBuffer,
|
||||
uint dwLength
|
||||
);
|
||||
|
||||
// --- Âñïîìîãàòåëüíûå Ìåòîäû ---
|
||||
|
||||
/// <summary>
|
||||
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ ôëàãà çàùèòû ïàìÿòè.
|
||||
/// </summary>
|
||||
public static string GetMemoryProtectString(uint protect)
|
||||
{
|
||||
// Çäåñü ìîæíî ðåàëèçîâàòü áîëåå äåòàëüíîå îòîáðàæåíèå ôëàãîâ çàùèòû.
|
||||
// Äëÿ ïðîñòîòû, ïîêà ïðîñòî âîçâðàùàåì øåñòíàäöàòåðè÷íîå çíà÷åíèå.
|
||||
// Â äàëüíåéøåì ìîæíî ðàñøèðèòü äëÿ PAGE_READONLY, PAGE_READWRITE è ò.ä.
|
||||
return $"0x{protect:X}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ ñîñòîÿíèÿ ïàìÿòè.
|
||||
/// </summary>
|
||||
public static string GetMemoryStateString(uint state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case MEM_COMMIT: return "MEM_COMMIT";
|
||||
case MEM_FREE: return "MEM_FREE";
|
||||
case MEM_RESERVE: return "MEM_RESERVE";
|
||||
default: return $"Unknown (0x{state:X})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ òèïà ïàìÿòè.
|
||||
/// </summary>
|
||||
public static string GetMemoryTypeString(uint type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MEM_IMAGE: return "MEM_IMAGE";
|
||||
case MEM_MAPPED: return "MEM_MAPPED";
|
||||
case MEM_PRIVATE: return "MEM_PRIVATE";
|
||||
default: return $"Unknown (0x{type:X})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ïðîâåðÿåò ïîñëåäíþþ îøèáêó WinAPI è âûáðàñûâàåò èñêëþ÷åíèå, åñëè îøèáêà åñòü.
|
||||
/// </summary>
|
||||
public static void ThrowWinApiErrorIfAny(string methodName)
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
if (errorCode != 0)
|
||||
{
|
||||
throw new ExternalException($"WinAPI call {methodName} failed with error code {errorCode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Agent": {
|
||||
"Monitoring": {
|
||||
"ScanIntervalSeconds": 5,
|
||||
"TargetProcessName": ""
|
||||
"MemoryRegionScanTimeout": 30
|
||||
},
|
||||
"WebServer": {
|
||||
"Url": "http://0.0.0.0:8080"
|
||||
@ -13,4 +13,4 @@
|
||||
"ProcessMemoryAgent.Services.ProcessMonitor": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<SupportedOSPlatform>windows</SupportedOSPlatform>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
@ -13,16 +14,17 @@
|
||||
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<Version>0.1.0</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<Company>OpenSource</Company>
|
||||
<Product>Process Monitoring Agent</Product>
|
||||
<Description>A service for detailed monitoring processes and memory regions.</Description>
|
||||
<Copyright>Copyright © 2024-2025 Gregory Lirent</Copyright>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user