v0.1.6: Added windows

This commit is contained in:
Gregory Lirent 2025-07-13 14:24:51 +03:00
parent c0ccca98cf
commit 37d4f290e3
9 changed files with 443 additions and 4 deletions

View File

@ -23,7 +23,7 @@ namespace WebmrAPI.Controllers
_monitor = monitor; _monitor = monitor;
_logger = logger; _logger = logger;
} }
private string GetFormattedJson<T>(T data, bool pretty) internal static string GetFormattedJson<T>(T data, bool pretty)
{ {
var options = new JsonSerializerOptions var options = new JsonSerializerOptions
{ {
@ -269,11 +269,53 @@ namespace WebmrAPI.Controllers
} }
} }
private static IEnumerable<T1> Sort<T1, T2>(IEnumerable<T1> data, Func<T1, T2> selector, bool desc) [HttpGet("{pid}/windows")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<BaseWindowInfo>))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
async public Task<IActionResult> GetProcessWindowsById(int pid,
[FromQuery] bool pretty = false,
[FromQuery] string sortBy = "",
[FromQuery] bool desc = false,
[FromQuery] int limit = 0,
[FromQuery] int offset = 0
)
{
try
{
var data = await _monitor.GetProcessWindows(pid);
if (data == null)
{
return NotFound($"The process with the PID {pid} was not found or its windows could not be obtained.");
}
if (!String.IsNullOrEmpty(sortBy))
{
sortBy = sortBy.ToLowerInvariant();
switch (sortBy)
{
case "title": data = ProcessController.Sort(data, p => p.Title, desc); break;
case "id": data = ProcessController.Sort(data, p => p.Id, desc); break;
default: return StatusCode(StatusCodes.Status400BadRequest, $"Unexpected search filter {sortBy}.");
}
}
return Content(GetFormattedJson(Paginate(data, limit, offset), pretty), "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, $"An error occurred while receiving threads for PID {pid}.");
return StatusCode(StatusCodes.Status500InternalServerError, $"An internal server error occurred while receiving threads for PID {pid}.");
}
}
internal static IEnumerable<T1> Sort<T1, T2>(IEnumerable<T1> data, Func<T1, T2> selector, bool desc)
{ {
return desc ? data.OrderByDescending(selector) : data.OrderBy(selector); return desc ? data.OrderByDescending(selector) : data.OrderBy(selector);
} }
private IEnumerable<T> Paginate<T>(IEnumerable<T> data, int limit, int offset) internal static IEnumerable<T> Paginate<T>(IEnumerable<T> data, int limit, int offset)
{ {
if (offset > 0) data = data.Skip(offset); if (offset > 0) data = data.Skip(offset);
if (limit > 0) data = data.Take(limit); if (limit > 0) data = data.Take(limit);

View File

@ -0,0 +1,68 @@
/* This software is licensed by the MIT License, see LICENSE file */
/* Copyright © 2024-2025 Gregory Lirent */
using Microsoft.AspNetCore.Mvc;
using WebmrAPI.Models;
using WebmrAPI.Services;
namespace WebmrAPI.Controllers
{
[ApiController]
[Route("api/v1/[controller]")]
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public class WindowsController : ControllerBase
{
private readonly ProcessMonitor _monitor;
private readonly ILogger<ProcessController> _logger;
public WindowsController(ProcessMonitor monitor, ILogger<ProcessController> logger)
{
_monitor = monitor;
_logger = logger;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<WindowInfo>))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetWindows(
[FromQuery] bool pretty = false,
[FromQuery] string sortBy = "",
[FromQuery] bool desc = false,
[FromQuery] int limit = 0,
[FromQuery] int offset = 0,
[FromQuery] string search = ""
)
{
try
{
var data = _monitor.GetBufferedWindows();
if (data != null && !String.IsNullOrEmpty(search))
{
data = data.Where(p => p.Title != null && p.Title.ToLowerInvariant().Contains(search.ToLowerInvariant()));
}
if (data != null && !String.IsNullOrEmpty(sortBy))
{
sortBy = sortBy.ToLowerInvariant();
switch (sortBy)
{
case "pid": data = ProcessController.Sort(data, p => p.PID, desc); break;
case "title": data = ProcessController.Sort(data, p => p.Title, desc); break;
case "threadid": data = ProcessController.Sort(data, p => p.ThreadId, desc); break;
case "id": data = ProcessController.Sort(data, p => p.Id, desc); break;
default: return StatusCode(StatusCodes.Status400BadRequest, $"Unexpected search filter {sortBy}.");
}
}
return Content(ProcessController.GetFormattedJson(ProcessController.Paginate(data, limit, offset), pretty), "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred when getting the list of windows");
return StatusCode(StatusCodes.Status500InternalServerError, "An internal server error occurred when receiving windows.");
}
}
}
}

View File

@ -10,6 +10,8 @@ namespace WebmrAPI.Models
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public class ProcessInfo : ProcessBaseInfo public class ProcessInfo : ProcessBaseInfo
{ {
private IEnumerable<BaseWindowInfo>? _windows;
[JsonIgnore] [JsonIgnore]
public LazyConcurrentContainer<MemoryRegionInfo> MemoryRegionsContainer { get; set; } = new(); public LazyConcurrentContainer<MemoryRegionInfo> MemoryRegionsContainer { get; set; } = new();
[JsonIgnore] [JsonIgnore]
@ -34,5 +36,12 @@ namespace WebmrAPI.Models
{ {
get => ThreadsContainer.Values; get => ThreadsContainer.Values;
} }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<BaseWindowInfo>? Windows
{
get => LockedGet(ref _windows);
internal set => LockedSet(ref _windows, value);
}
} }
} }

View File

@ -10,6 +10,7 @@ namespace WebmrAPI.Models
{ {
private TimeSpan _lastPTime = TimeSpan.Zero; private TimeSpan _lastPTime = TimeSpan.Zero;
private TimeSpan _curPTime = TimeSpan.Zero; private TimeSpan _curPTime = TimeSpan.Zero;
private IEnumerable<BaseWindowInfo>? _windows;
private int _id = 0; private int _id = 0;
private int _currentPriority = 0; private int _currentPriority = 0;
@ -49,5 +50,12 @@ namespace WebmrAPI.Models
get => LockedGet(ref _cpuUsage); get => LockedGet(ref _cpuUsage);
set => LockedSet(ref _cpuUsage, value); set => LockedSet(ref _cpuUsage, value);
} }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<BaseWindowInfo>? Windows
{
get => LockedGet(ref _windows);
internal set => LockedSet(ref _windows, value);
}
} }
} }

122
Models/WindowInfo.cs Normal file
View File

@ -0,0 +1,122 @@
using System.Drawing;
using System.Text.Json.Serialization;
using WebmrAPI.Utils;
namespace WebmrAPI.Models
{
public class GeometryPoint
{
internal Point _point;
public static GeometryPoint Empty { get => new GeometryPoint(Point.Empty); }
public int X { get => _point.X; }
public int Y { get => _point.Y; }
public GeometryPoint(int x, int y)
{
_point = new Point(x, y);
}
public GeometryPoint(Point point)
{
_point = point;
}
}
public class BaseGeometry
{
protected Rectangle _rectangle;
public int Width { get => _rectangle.Width; }
public int Height { get => _rectangle.Height; }
public static BaseGeometry Empty { get => new BaseGeometry { _rectangle = Rectangle.Empty }; }
public static Geometry FromLTRB(int left, int top, int right, int bottom)
{
return new Geometry
{
_rectangle = Rectangle.FromLTRB(left, top, right, bottom)
};
}
public bool Contains(GeometryPoint pt)
{
return _rectangle.Contains(pt._point);
}
}
public class Geometry : BaseGeometry
{
public int X { get => _rectangle.X; }
public int Y { get => _rectangle.Y; }
public new static Geometry Empty { get => new Geometry { _rectangle = Rectangle.Empty }; }
}
public class BaseWindowInfo : ConcurrentObject
{
private IntPtr _hwnd;
private string _title = String.Empty;
private GeometryPoint _cursor = GeometryPoint.Empty;
private Geometry _gWindow = Geometry.Empty;
private BaseGeometry _gContent = BaseGeometry.Empty;
private bool _isActive;
public string Id
{
get => LockedGet(ref _hwnd).ToString();
set
{
if (!IntPtr.TryParse(value, out IntPtr hwnd))
throw new ArgumentException("Invalid window Id format. Must be a valid hwnd (IntPtr) string representation");
LockedSet(ref _hwnd, hwnd);
}
}
public string Title
{
get => LockedGet(ref _title);
set => LockedSet(ref _title, value);
}
public GeometryPoint CursorPosition
{
get => LockedGet(ref _cursor);
set => LockedSet(ref _cursor, value);
}
public Geometry WindowGeometry
{
get => LockedGet(ref _gWindow);
set => LockedSet(ref _gWindow, value);
}
public BaseGeometry ContentGeometry
{
get => LockedGet(ref _gContent);
set => LockedSet(ref _gContent, value);
}
public bool IsActive
{
get => LockedGet(ref _isActive);
set => LockedSet(ref _isActive, value);
}
[JsonIgnore]
public IntPtr Hwnd
{
get => LockedGet(ref _hwnd);
set => LockedSet(ref _hwnd, value);
}
public bool HasCursor { get => IsActive && ContentGeometry.Contains(CursorPosition); }
}
public class WindowInfo : BaseWindowInfo
{
private int _pid;
private int _threadId;
public int PID
{
get => LockedGet(ref _pid);
set => LockedSet(ref _pid, value);
}
public int ThreadId
{
get => LockedGet(ref _threadId);
set => LockedSet(ref _threadId, value);
}
}
}

View File

@ -2,6 +2,7 @@
/* Copyright © 2024-2025 Gregory Lirent */ /* Copyright © 2024-2025 Gregory Lirent */
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using WebmrAPI.Configuration; using WebmrAPI.Configuration;
using WebmrAPI.Exceptions; using WebmrAPI.Exceptions;
@ -17,16 +18,22 @@ namespace WebmrAPI.Services
private readonly ILogger<ProcessMonitor> _logger; private readonly ILogger<ProcessMonitor> _logger;
private readonly MonitoringSettings _config; private readonly MonitoringSettings _config;
private LazyConcurrentContainer<ProcessInfo> _processes = new(); private LazyConcurrentContainer<ProcessInfo> _processes = new();
private LazyConcurrentContainer<WindowInfo> _windows = new();
private ScanProvider _provider; private ScanProvider _provider;
public ILogger<ProcessMonitor> Logger { get => _logger; } public ILogger<ProcessMonitor> Logger { get => _logger; }
public MonitoringSettings Config { get => _config; } public MonitoringSettings Config { get => _config; }
public LazyConcurrentContainer<ProcessInfo> Processes { get => _processes; } public LazyConcurrentContainer<ProcessInfo> Processes { get => _processes; }
public LazyConcurrentContainer<WindowInfo> Windows { get => _windows; }
public IEnumerable<ProcessBaseInfo>? GetBufferedProcesses() public IEnumerable<ProcessBaseInfo>? GetBufferedProcesses()
{ {
return _processes.Values; return _processes.Values;
} }
public IEnumerable<WindowInfo>? GetBufferedWindows()
{
return _windows.Values;
}
async public Task<ProcessInfo?> GetProcessDetails(int pid, ScanTarget target = ScanTarget.ProcessDetails) async public Task<ProcessInfo?> GetProcessDetails(int pid, ScanTarget target = ScanTarget.ProcessDetails)
{ {
@ -50,6 +57,18 @@ namespace WebmrAPI.Services
return null; return null;
} }
public Task<IEnumerable<BaseWindowInfo>?> GetProcessWindows(int pid)
{
return Task.Run(() =>
{
if (_processes.Container != null && _processes.Container.TryGetValue(pid, out var info))
{
return info.Windows;
}
return null;
});
}
public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> settings) public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> settings)
{ {
@ -88,6 +107,46 @@ namespace WebmrAPI.Services
try try
{ {
await _provider.CreateScanTask().ScanAsync(); await _provider.CreateScanTask().ScanAsync();
await new WindowScanner(_provider, _windows).ScanAsync();
Dictionary<int, List<BaseWindowInfo>> pWin = new();
Dictionary<int, List<BaseWindowInfo>> tWin = new();
if (_windows.Values != null)
foreach (var window in _windows.Values)
{
List<BaseWindowInfo> p;
List<BaseWindowInfo> t;
if (!pWin.TryGetValue(window.PID, out p))
{
pWin.Add(window.PID, p = new());
}
if (!tWin.TryGetValue(window.ThreadId, out t))
{
tWin.Add(window.ThreadId, t = new());
}
p.Add(window);
t.Add(window);
}
if (_processes.Values != null)
foreach (var proc in _processes.Values)
{
if (pWin.TryGetValue(proc.PID, out var pws))
{
proc.Windows = pws;
if (proc.ThreadsContainer.Values != null)
foreach (var thread in proc.ThreadsContainer.Values)
{
if (tWin.TryGetValue(thread.ID, out var tws))
{
thread.Windows = tws;
}
}
}
}
_logger.LogInformation($"Process buffer updated, contains {Processes.Container?.Count} processes."); _logger.LogInformation($"Process buffer updated, contains {Processes.Container?.Count} processes.");
} }
catch (ProcessMonitorException ex) catch (ProcessMonitorException ex)

View File

@ -10,6 +10,7 @@ namespace WebmrAPI.Services.Scanners
MemoryRegions = 0x02, MemoryRegions = 0x02,
Modules = 0x04, Modules = 0x04,
Threads = 0x08, Threads = 0x08,
Windows = 0x10,
ProcessDetails = MemoryRegions | Modules | Threads, ProcessDetails = MemoryRegions | Modules | Threads,
All = Processes | ProcessDetails All = Processes | ProcessDetails

View File

@ -0,0 +1,130 @@
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using WebmrAPI.Models;
using WebmrAPI.Utils;
namespace WebmrAPI.Services.Scanners
{
public class WindowScanner : AbstractScanner<WindowInfo>
{
[StructLayout(LayoutKind.Explicit)]
private struct RECT
{
[FieldOffset(0)]
public int Left;
[FieldOffset(4)]
public int Top;
[FieldOffset(8)]
public int Right;
[FieldOffset(12)]
public int Bottom;
[FieldOffset(0)]
public int X;
[FieldOffset(4)]
public int Y;
}
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out RECT lpPoint);
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ScreenToClient(IntPtr hWnd, ref RECT lpPoint);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
override public ScanTarget Target { get => ScanTarget.Windows; }
private bool GetWindowInfo(WindowInfo info, IntPtr hwnd)
{
int length = GetWindowTextLength(hwnd);
if (length > 0)
{
var sb = new StringBuilder(length + 1);
GetWindowText(hwnd, sb, sb.Capacity);
info.Title = sb.ToString();
if (GetCursorPos(out RECT pt) && ScreenToClient(hwnd, ref pt))
{
info.CursorPosition = new GeometryPoint(pt.X, pt.Y);
}
if (GetWindowRect(hwnd, out RECT wRect))
{
info.WindowGeometry = Geometry.FromLTRB(wRect.Left, wRect.Top, wRect.Right, wRect.Bottom);
}
if (GetClientRect(hwnd, out RECT cRect))
{
info.ContentGeometry = BaseGeometry.FromLTRB(cRect.Left, cRect.Top, cRect.Right, cRect.Bottom);
}
return true;
}
return false;
}
override internal Task<bool> ScanAsync(Dictionary<long, WindowInfo> data)
{
return Task.Run(() =>
{
var foreground = GetForegroundWindow();
EnumWindows(delegate (IntPtr hwnd, IntPtr lParam)
{
if (IsWindowVisible(hwnd))
{
WindowInfo? info;
if (!GetFromCacheOrNew(hwnd.ToInt64(), out info))
{
info.Hwnd = hwnd;
int threadId;
if ((threadId = GetWindowThreadProcessId(hwnd, out int pid)) != 0)
{
info.PID = pid;
info.ThreadId = threadId;
}
else
{
info = null;
}
}
if (info != null && GetWindowInfo(info, hwnd))
{
info.IsActive = foreground == info.Hwnd;
data.Add(hwnd.ToInt64(), info);
}
}
return true;
}, IntPtr.Zero);
return true;
});
}
public WindowScanner(IScanProvider scanner, LazyConcurrentContainer<WindowInfo> container)
: base(scanner, container) { }
}
}

View File

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