Files
git.stella-ops.org/src/Plugin/StellaOps.Plugin.Sandbox/Resources/WindowsResourceLimiter.cs
2026-02-01 21:37:40 +02:00

355 lines
12 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using SystemProcess = System.Diagnostics.Process;
namespace StellaOps.Plugin.Sandbox.Resources;
/// <summary>
/// Windows Job Object resource limiter implementation.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class WindowsResourceLimiter : IResourceLimiter, IDisposable
{
private readonly ILogger<WindowsResourceLimiter> _logger;
private readonly Dictionary<int, SafeFileHandle> _jobHandles = new();
private readonly object _lock = new();
private bool _disposed;
/// <summary>
/// Creates a new Windows resource limiter.
/// </summary>
public WindowsResourceLimiter(ILogger<WindowsResourceLimiter> logger)
{
_logger = logger;
}
/// <inheritdoc />
public ResourceConfiguration CreateConfiguration(ResourceLimits limits)
{
var cpuQuotaUs = limits.MaxCpuPercent > 0
? (long)(limits.MaxCpuPercent / 100.0 * 100_000)
: 0;
return new ResourceConfiguration
{
MemoryLimitBytes = limits.MaxMemoryMb * 1024 * 1024,
CpuQuotaUs = cpuQuotaUs,
CpuPeriodUs = 100_000,
MaxProcesses = limits.MaxProcesses,
MaxOpenFiles = limits.MaxOpenFiles
};
}
/// <inheritdoc />
public Task ApplyLimitsAsync(
SystemProcess process,
ResourceConfiguration config,
CancellationToken ct)
{
if (!OperatingSystem.IsWindows())
{
_logger.LogWarning("WindowsResourceLimiter called on non-Windows platform");
return Task.CompletedTask;
}
var jobName = $"StellaOps_Sandbox_{process.Id}";
try
{
// Create Job Object
var jobHandle = NativeMethods.CreateJobObject(IntPtr.Zero, jobName);
if (jobHandle.IsInvalid)
{
throw new InvalidOperationException(
$"Failed to create Job Object: {Marshal.GetLastWin32Error()}");
}
// Configure limits
var extendedInfo = new NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
extendedInfo.BasicLimitInformation.LimitFlags = 0;
// Memory limit
if (config.MemoryLimitBytes > 0)
{
extendedInfo.ProcessMemoryLimit = (UIntPtr)config.MemoryLimitBytes;
extendedInfo.JobMemoryLimit = (UIntPtr)config.MemoryLimitBytes;
extendedInfo.BasicLimitInformation.LimitFlags |=
NativeMethods.JOB_OBJECT_LIMIT_PROCESS_MEMORY |
NativeMethods.JOB_OBJECT_LIMIT_JOB_MEMORY;
}
// Process limit
if (config.MaxProcesses > 0)
{
extendedInfo.BasicLimitInformation.ActiveProcessLimit = (uint)config.MaxProcesses;
extendedInfo.BasicLimitInformation.LimitFlags |=
NativeMethods.JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
}
// Apply extended limits
var extendedInfoSize = Marshal.SizeOf<NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
var success = NativeMethods.SetInformationJobObject(
jobHandle,
NativeMethods.JobObjectInfoType.ExtendedLimitInformation,
ref extendedInfo,
extendedInfoSize);
if (!success)
{
jobHandle.Dispose();
throw new InvalidOperationException(
$"Failed to set Job Object limits: {Marshal.GetLastWin32Error()}");
}
// Configure CPU rate control (Windows 8+)
if (config.CpuQuotaUs > 0)
{
var cpuRate = (uint)(config.CpuQuotaUs / (double)config.CpuPeriodUs * 10000);
var cpuInfo = new NativeMethods.JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
{
ControlFlags = NativeMethods.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE |
NativeMethods.JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP,
CpuRate = cpuRate
};
var cpuInfoSize = Marshal.SizeOf<NativeMethods.JOBOBJECT_CPU_RATE_CONTROL_INFORMATION>();
NativeMethods.SetInformationJobObject(
jobHandle,
NativeMethods.JobObjectInfoType.CpuRateControlInformation,
ref cpuInfo,
cpuInfoSize);
// CPU rate control may fail on older Windows versions - non-fatal
}
// Assign process to Job Object
success = NativeMethods.AssignProcessToJobObject(jobHandle, process.Handle);
if (!success)
{
jobHandle.Dispose();
throw new InvalidOperationException(
$"Failed to assign process to Job Object: {Marshal.GetLastWin32Error()}");
}
lock (_lock)
{
_jobHandles[process.Id] = jobHandle;
}
_logger.LogDebug(
"Applied Job Object limits to process {ProcessId}: Memory={MemoryBytes}B",
process.Id,
config.MemoryLimitBytes);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to apply Job Object limits to process {ProcessId}", process.Id);
throw;
}
return Task.CompletedTask;
}
/// <inheritdoc />
public Task RemoveLimitsAsync(SystemProcess process, CancellationToken ct)
{
if (!OperatingSystem.IsWindows())
return Task.CompletedTask;
SafeFileHandle? jobHandle;
lock (_lock)
{
if (!_jobHandles.TryGetValue(process.Id, out jobHandle))
return Task.CompletedTask;
_jobHandles.Remove(process.Id);
}
try
{
// Terminate job object (will terminate all processes in job)
NativeMethods.TerminateJobObject(jobHandle, 0);
jobHandle.Dispose();
_logger.LogDebug("Removed Job Object for process {ProcessId}", process.Id);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to cleanup Job Object for process {ProcessId}", process.Id);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<ResourceUsage> GetUsageAsync(SystemProcess process, CancellationToken ct)
{
if (!OperatingSystem.IsWindows())
{
return Task.FromResult(ResourceUsage.Empty);
}
try
{
process.Refresh();
return Task.FromResult(new ResourceUsage
{
MemoryUsageMb = process.WorkingSet64 / (1024.0 * 1024.0),
CpuUsagePercent = GetCpuUsage(process),
ProcessCount = process.Threads.Count,
OpenFileHandles = process.HandleCount,
Timestamp = DateTimeOffset.UtcNow
});
}
catch
{
return Task.FromResult(ResourceUsage.Empty);
}
}
/// <inheritdoc />
public async Task<LimitCheckResult> CheckLimitsAsync(
SystemProcess process,
ResourceLimits limits,
CancellationToken ct)
{
var usage = await GetUsageAsync(process, ct);
// Check memory
if (limits.MaxMemoryMb > 0 && usage.MemoryUsageMb > limits.MaxMemoryMb)
{
return LimitCheckResult.Exceeded(
ResourceType.Memory,
usage.MemoryUsageMb,
limits.MaxMemoryMb);
}
// Check CPU
if (limits.MaxCpuPercent > 0 && usage.CpuUsagePercent > limits.MaxCpuPercent)
{
return LimitCheckResult.Exceeded(
ResourceType.Cpu,
usage.CpuUsagePercent,
limits.MaxCpuPercent);
}
return LimitCheckResult.Ok;
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
lock (_lock)
{
foreach (var handle in _jobHandles.Values)
{
handle.Dispose();
}
_jobHandles.Clear();
}
}
private static double GetCpuUsage(SystemProcess process)
{
// Simplified CPU usage - accurate measurement requires time-based sampling
try
{
return process.TotalProcessorTime.TotalMilliseconds /
(Environment.ProcessorCount * process.TotalProcessorTime.TotalMilliseconds + 1) * 100;
}
catch
{
return 0;
}
}
private static class NativeMethods
{
public const uint JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100;
public const uint JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200;
public const uint JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
public const uint JOB_OBJECT_CPU_RATE_CONTROL_ENABLE = 0x00000001;
public const uint JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP = 0x00000004;
public enum JobObjectInfoType
{
BasicLimitInformation = 2,
ExtendedLimitInformation = 9,
CpuRateControlInformation = 15
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
{
public uint ControlFlags;
public uint CpuRate;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern SafeFileHandle CreateJobObject(IntPtr lpJobAttributes, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetInformationJobObject(
SafeFileHandle hJob,
JobObjectInfoType infoType,
ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo,
int cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetInformationJobObject(
SafeFileHandle hJob,
JobObjectInfoType infoType,
ref JOBOBJECT_CPU_RATE_CONTROL_INFORMATION lpJobObjectInfo,
int cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool AssignProcessToJobObject(SafeFileHandle hJob, IntPtr hProcess);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool TerminateJobObject(SafeFileHandle hJob, uint uExitCode);
}
}