355 lines
12 KiB
C#
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);
|
|
}
|
|
}
|